MinIO STS AssumeRole 深度解析:临时凭证 API 的规范、实战与源码实现
本文以 MinIO 官方文档 assume-role.md 为核心,完整讲解 STS(Security Token Service)AssumeRole API 的参数规范、请求/响应格式与 awscli、Go 客户端的实战用法,并结合 cmd/sts-handlers.go 的服务端实现与 internal/config/identity/openid/openid.go 中的有效期计算逻辑,说明临时凭证从签发、策略继承到失效的完整链路。读完本文,你可以直接在 MinIO 上用 AWS CLI 或 minio-go 获取受限时效的临时凭证,并能读懂服务端每一步鉴权与签发动作背后的代码。
一、AssumeRole 是什么:用长期凭证换取临时凭证
AssumeRole 返回一组临时安全凭证,用于访问 MinIO 资源。它与 WebIdentity、AD/LDAP 等联邦认证方式不同:AssumeRole 要求请求方本身就是一个已存在的 MinIO 用户(持有 access key / secret key),然后用这组长期凭证"交换"出一组带生命周期的临时凭证。返回的临时凭证由三部分组成:
- Access Key(临时访问密钥)
- Secret Key(临时秘密密钥)
- Security Token(会话令牌)
应用可以用这组临时凭证对所有 MinIO API 操作进行签名。临时凭证所应用的安全策略继承自发起请求的 MinIO 用户;默认有效期为 1 小时,可通过可选的 DurationSeconds 参数指定,取值范围为 900 秒(15 分钟)到 365 天。
MinIO 官方文档给出的两个典型动机:
- 免去多部件上传的预签名难题:各客户端 SDK 的多部件上传流程复杂(含大量容错处理),而通用 SDK 并不支持"逐 URL 预签名"的多部件上传。持有临时凭证后,SDK 可以原生地完成整个 Multipart 流程,无需自己为每个 part 重新发明签名逻辑。
- 简化前缀级上传:客户端拿到会话凭证后,可以自行上传整个目录/前缀下的文件,服务端应用不必为每个文件生成并下发一条预签名 URL。
MinIO STS 快速入门文档 docs/sts/README.md 还强调了临时凭证的三大优势,这同样适用于 AssumeRole 场景:
- 临时凭证是短生命周期的,配置从几分钟到数小时不等,过期后 MinIO 不再识别它,任何携带该凭证的 API 请求都会被拒绝;
- 临时凭证不需要随应用长期存储,而是按需动态生成,到期前应用可再次申请;
- 临时凭证有明确的生命周期上限,无需轮换、也无需显式吊销——过期的临时凭证不可复用。
在 MinIO STS 的联邦认证矩阵中,AssumeRole 的定位是:
| 认证方式 | 说明 |
|---|---|
| WebIdentity | 使用任意 OIDC 兼容身份提供方(Keycloak、Dex、Google 等)的 Web 身份令牌换取临时凭证 |
| AD/LDAP | AD/LDAP 用户使用用户名和密码换取临时凭证 |
| AssumeRole | MinIO 用户使用自身的 access key / secret key 换取临时凭证 |
二、API 请求参数逐项解析
AssumeRole 是一个 POST 请求。cmd/sts-handlers.go 中的路由注册表明,服务端通过三个条件识别该请求:Content-Type 为 application/x-www-form-urlencoded、Authorization 头为 SigV4 签名、且 URL 上不带查询参数(参数全部放在 form body 中)。这一点与 AssumeRoleWithWebIdentity 等 action 通过 ?Action=... 查询参数路由的机制不同。
2.1 Version(必填)
STS API 版本信息,唯一支持的值为 2011-06-15。该值借用自 AWS STS API 以兼容 AWS 生态。
| 属性 | 值 |
|---|---|
| 类型 | String |
| 是否必填 | 是 |
服务端在 AssumeRole 处理器 中校验该字段,不匹配 stsAPIVersion = "2011-06-15" 时返回 ErrSTSMissingParameter 错误。
2.2 AUTHPARAMS(SigV4 授权头)
AssumeRole 不支持匿名请求,必须携带与 AWS Signature V4 兼容的 Authorization 头,用已有 MinIO 用户的 access key / secret key 完成签名。服务端校验链路见 checkAssumeRoleAuth:
func checkAssumeRoleAuth(ctx context.Context, r *http.Request) (auth.Credentials, APIErrorCode) {
if !isRequestSignatureV4(r) {
return auth.Credentials{}, ErrAccessDenied
}
s3Err := isReqAuthenticated(ctx, r, globalSite.Region(), serviceSTS)
// ...
// Temporary credentials or Service accounts cannot generate further
// temporary credentials.
if user.IsTemp() || user.IsServiceAccount() {
return auth.Credentials{}, ErrAccessDenied
}
// Session tokens are not allowed in STS AssumeRole requests.
if getSessionToken(r) != "" {
return auth.Credentials{}, ErrAccessDenied
}
return user, ErrNone
}
从源码可以确认两条文档未明说、但实操中会遇到的硬限制:
- 临时凭证不能再生成临时凭证:如果签名所用的 access key 本身是 STS 签发的临时凭证或服务账号,请求直接被拒绝(
ErrAccessDenied),即禁止凭证"套娃"; - 请求中不允许携带 session token:带
x-amz-security-token的 AssumeRole 请求会被拒绝。
2.3 DurationSeconds(可选)
临时凭证的有效期(秒)。取值范围为 900(15 分钟)到 31536000(365 天),默认 3600 秒。超出范围操作直接失败。
| 属性 | 值 |
|---|---|
| 类型 | Integer |
| 有效范围 | 最小 900,最大 31536000 |
| 是否必填 | 否 |
这个边界在源码中是精确落地的。internal/config/constants.go 定义:
// either the exp claim or MINI_STS_DURATION value
const (
MinExpiration = 900
MaxExpiration = 31536000
)
而真正的有效期计算发生在 GetDefaultExpiration:
// GetDefaultExpiration - returns the expiration seconds expected.
func GetDefaultExpiration(dsecs string) (time.Duration, error) {
timeout := env.Get(config.EnvMinioStsDuration, "")
defaultExpiryDuration, err := time.ParseDuration(timeout)
if err != nil {
defaultExpiryDuration = time.Hour
}
if timeout == "" && dsecs != "" {
expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
if err != nil {
return 0, auth.ErrInvalidDuration
}
if expirySecs < config.MinExpiration || expirySecs > config.MaxExpiration {
return 0, auth.ErrInvalidDuration
}
defaultExpiryDuration = time.Duration(expirySecs) * time.Second
} else if timeout == "" && dsecs == "" {
return time.Hour, nil
}
if defaultExpiryDuration.Seconds() < config.MinExpiration || defaultExpiryDuration.Seconds() > config.MaxExpiration {
return 0, auth.ErrInvalidDuration
}
return defaultExpiryDuration, nil
}
由此可以推断出三条实际行为规则:
- 默认 1 小时:未设置环境变量、也未传
DurationSeconds时,返回time.Hour,与文档描述一致; MINIO_STS_DURATION环境变量优先:设置了该变量(Go duration 格式)时,它直接覆盖请求方传入的DurationSeconds,并且同样受 900–31536000 秒的范围约束。这意味着管理员可以在服务端一刀切地限制所有 STS 会话的长度,客户端无法申请更长;- 越界即报错:
DurationSeconds小于 900 或大于 31536000 都会得到auth.ErrInvalidDuration,最终映射为InvalidParameterValue类的 STS 错误响应。
2.4 Policy(可选会话策略)
一个 JSON 格式的 IAM 策略,作为内联会话策略(session policy)。传参后返回的临时凭证的权限是"用户的既有策略"与"会话策略"的交集——不能用会话策略授予超出用户已有策略的权限。
| 属性 | 值 |
|---|---|
| 类型 | String |
| 有效范围 | 最小长度 1,最大长度 2048 |
| 是否必填 | 否 |
服务端解析逻辑见 populateSessionPolicy,这里有两个文档级描述之外的细节:
func (c stsClaims) populateSessionPolicy(form url.Values) error {
// ...
sessionPolicy, err := policy.ParseConfig(bytes.NewReader([]byte(sessionPolicyStr)))
if err != nil {
return err
}
// Version in policy must not be empty
if sessionPolicy.Version == "" {
return errors.New("Version cannot be empty expecting '2012-10-17'")
}
policyBuf, err := json.Marshal(sessionPolicy)
// ...
// The plain text that you use for both inline and managed session
// policies shouldn't exceed maxSTSSessionPolicySize characters.
if len(policyBuf) > maxSTSSessionPolicySize {
return errSessionPolicyTooLarge
}
c[policy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
return nil
}
- 策略 JSON 中
Version字段不能为空,期望值为2012-10-17,否则返回参数错误; - 2048 的实际上限是
maxSTSSessionPolicySize = 2048(cmd/sts-handlers.go),校验的是重新序列化后的 JSON 字节长度,而非原始字符串长度; - 校验通过后,会话策略以 Base64 编码写入 JWT 的 claims 中,随会话令牌一起下发。
2.5 示例 POST 请求
原文档给出的原始请求示例(AUTHPARAMS 代表 SigV4 签名产生的请求头集合):
http://minio:9000/?Action=AssumeRole&DurationSeconds=3600&Version=2011-06-15&Policy={"Version":"2012-10-17","Statement":[{"Sid":"Stmt1","Effect":"Allow","Action":"s3:*","Resource":"arn:aws:s3:::*"}]}&AUTHPARAMS
三、响应结构:XML 响应与数据模型
响应 XML 与 AWS STS AssumeRole 的响应元素保持一致。原文档给出的完整样例:
<?xml version="1.0" encoding="UTF-8"?>
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<AssumedRoleUser>
<Arn/>
<AssumeRoleId/>
</AssumedRoleUser>
<Credentials>
<AccessKeyId>Y4RJU1RNFGK48LGO9I2S</AccessKeyId>
<SecretAccessKey>sYLRKS1Z7hSjluf6gEbb9066hnx315wHTiACPAjg</SecretAccessKey>
<Expiration>2019-08-08T20:26:12Z</Expiration>
<SessionToken>eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJZNFJKVTFSTkZHSzQ4TEdPOUkyUyIs...</SessionToken>
</Credentials>
</AssumeRoleResult>
<ResponseMetadata>
<RequestId>c6104cbe-af31-11e0-8154-cbc7ccf896c7</RequestId>
</ResponseMetadata>
</AssumeRoleResponse>
与之对应的 Go 数据模型定义在 cmd/sts-datatypes.go:
// AssumeRoleResponse contains the result of successful AssumeRole request.
type AssumeRoleResponse struct {
XMLName xml.Name `xml:"https://sts.amazonaws.com/doc/2011-06-15/ AssumeRoleResponse" json:"-"`
Result AssumeRoleResult `xml:"AssumeRoleResult"`
ResponseMetadata struct {
RequestID string `xml:"RequestId,omitempty"`
} `xml:"ResponseMetadata,omitempty"`
}
// AssumeRoleResult - Contains the response to a successful AssumeRole
// request, including temporary credentials that can be used to make
// MinIO API requests.
type AssumeRoleResult struct {
AssumedRoleUser AssumedRoleUser `xml:",omitempty"`
Credentials auth.Credentials `xml:",omitempty"`
PackedPolicySize int `xml:",omitempty"`
}
两个可以注意的实现事实:
AssumedRoleUser中的Arn/AssumeRoleId在 MinIO 中恒为空(响应样例中即为空标签)。这与 awscli 测试输出一致:--role-arn和--role-session-name对 MinIO 没有实际意义,填满足 CLI 必填校验的任意值即可;SessionToken是一个 JWT(从样例中eyJhbGciOiJIUzUxMiI...的头部可解出alg: HS512),服务端后续正是靠校验这个 JWT 来完成临时凭证的鉴权。ResponseMetadata.RequestId则取自响应的x-amz-request-id头(见 AssumeRole 处理器)。
四、服务端签发链路:一次 AssumeRole 请求在源码中的完整旅程
AssumeRole 处理器 的处理顺序,可以按以下六步理解:
第 1 步:鉴权(并刻意延迟报错以保证审计日志)
处理器开头先调用 checkAssumeRoleAuth 拿到签名用户,但不立即返回失败。源码注释解释了原因:要等确认这是一个合法的 STS 请求之后再失败,这样才能输出一条合适的审计日志(defer logger.AuditLog(ctx, w, r, claims))。随后解析 form、校验 Version 和 Action=AssumeRole。
第 2 步:解析会话策略
调用 claims.populateSessionPolicy(r.Form),即 2.4 节所述的解析与 2048 字节上限校验,成功后策略以 Base64 进入 claims。
第 3 步:计算有效期并写入 claims
duration, err := openid.GetDefaultExpiration(r.Form.Get(stsDurationSeconds))
// ...
claims[expClaim] = UTCNow().Add(duration).Unix()
claims[parentClaim] = user.AccessKey
exp 为标准 JWT 过期声明,parent 声明记录发起请求的父用户 access key——这就是"临时凭证策略继承自父用户"的数据基础。此外还支持可选的 TokenRevokeType 参数写入 tokenRevokeType 声明,用于支持令牌吊销。
第 4 步:确认父用户的策略可解析
// Validate that user.AccessKey's policies can be retrieved - it may not
// be in case the user is disabled.
if _, err = globalIAMSys.PolicyDBGet(user.AccessKey, user.Groups...); err != nil {
writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue, err)
return
}
被禁用或策略不可解析的用户拿不到临时凭证,这一步把"用户被禁用"的语义提前拦截在签发之前。
第 5 步:生成凭证并注册临时用户
secret, err := getTokenSigningKey()
// ...
cred, err := auth.GetNewCredentialsWithMetadata(claims, secret)
// ...
// Set the parent of the temporary access key, so that it's access
// policy is inherited from `user.AccessKey`.
cred.ParentUser = user.AccessKey
// Set the newly generated credentials.
updatedAt, err := globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, "")
三个关键点:
- 签名密钥来自 getTokenSigningKey:默认使用根凭据的
SecretKey;若启用了站点复制(Site Replication),则改用站点复制专用密钥。这保证了同一站点群的副本能解析同一批 STS 令牌; cred.ParentUser显式设置为父用户,临时凭证鉴权时按"父用户策略 ∩ 会话策略"求交集;globalIAMSys.SetTempUser将临时用户注册进 IAM 系统(注意 AssumeRole 分支传入的 policyName 为空字符串,策略完全靠 ParentUser 继承)。
第 6 步:站点复制钩子与响应
如果父用户不是根账号,会调用 globalSiteReplicationSys.IAMChangeHook,把新签发的 STS 凭证(access key、secret key、session token、parent user、更新时间)以 SRIAMItemSTSAcc 类型推送到复制对端——从源码结构看,这保证分布式站点之间对同一组临时凭证的识别状态一致。最后组装 AssumeRoleResponse 并以 XML 写出。
五、错误映射:文档中"类似 AWS STS"的具体含义
原文档说明错误响应"与 AWS STS AssumeRole 类似"。结合 apiToSTSError 可以看到 MinIO 内部 API 错误码到 STS 错误码的精确映射:
func apiToSTSError(authErr APIErrorCode) (stsErrCode STSErrorCode) {
switch authErr {
case ErrSignatureDoesNotMatch, ErrInvalidAccessKeyID, ErrAccessKeyDisabled:
return ErrSTSAccessDenied
case ErrServerNotInitialized:
return ErrSTSNotInitialized
case ErrInternalError:
return ErrSTSInternalError
default:
return ErrSTSAccessDenied
}
}
也就是说:签名不匹配、access key 无效或该 key 被禁用,统一返回 AccessDenied;服务器未初始化返回 NotInitialized;其余内部异常返回 InternalError。参数层面的问题(版本号错误、策略超 2048、DurationSeconds 越界、策略 Version 为空等)则以 InvalidParameterValue 类错误返回。错误码常量定义在 cmd/sts-errors.go。
六、动手实操:从零验证 AssumeRole
6.1 启动 MinIO 服务
按原文档给出的最小命令启动(需要已创建的多用户账号,参考 MinIO 多用户管理指南创建用户并绑定策略):
export MINIO_ROOT_USER=minio
export MINIO_ROOT_PASSWORD=minio123
minio server ~/test
如果想让服务端约束 STS 会话时长上限,可同时设置 MINIO_STS_DURATION(Go duration 格式,如 MINIO_STS_DURATION=2h),此时所有 AssumeRole 请求的实际有效期以该值为准(见 2.3 节源码分析)。
6.2 使用 AWS CLI 测试
配置 ~/.aws/credentials(使用上一步创建的普通用户,而非 root):
[foobar]
region = us-east-1
aws_access_key_id = foobar
aws_secret_access_key = foo12345
然后执行:
$ aws --profile foobar --endpoint-url http://localhost:9000 sts assume-role --policy '{"Version":"2012-10-17","Statement":[{"Sid":"Stmt1","Effect":"Allow","Action":"s3:*","Resource":"arn:aws:s3:::*"}]}' --role-arn arn:xxx:xxx:xxx:xxxx --role-session-name anything
{
"AssumedRoleUser": {
"Arn": ""
},
"Credentials": {
"SecretAccessKey": "xbnWUoNKgFxi+uv3RI9UgqP3tULQMdI+Hj+4psd4",
"SessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJLOURUSU1VVlpYRVhKTDNBVFVPWSIsImV4cCI6MzYwMDAwMDAwMDAwMCwicG9saWN5IjoidGVzdCJ9.PetK5wWUcnCJkMYv6TEs7HqlA4x_vViykQ8b2T_6hapFGJTO34sfTwqBnHF6lAiWxRoZXco11B0R7y58WAsrQw",
"Expiration": "2019-02-20T19:56:59-08:00",
"AccessKeyId": "K9DTIMUVZXEXJL3ATUOY"
}
}
注意 --role-arn 与 --role-session-name 在 MinIO 中无实际语义,只需满足 CLI 的必填校验;响应中 AssumedRoleUser.Arn 为空正是 6.2 节数据模型行为的直接体现。
6.3 使用仓库内置的 assume-role.go 客户端测试
仓库附带了 docs/sts/assume-role.go 示例程序(文件头带 //go:build ignore,作为独立 main 包运行),它演示了 minio-go 的 STS 凭证链用法:
$ go run docs/sts/assume-role.go -u foobar -p foo12345 -d
Only displaying credentials:
AccessKeyID: 27YDRYEM0S9B44AJJX9X
SecretAccessKey: LHPdHeaLiYk+pDZ3hgN3sdwXpJC2qbhBfZ8ii9Z3
SessionToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiIyN1lEUllFTTBTOUI0NEFKSlg5WCIsImV4cCI6MzYwMDAwMDAwMDAwMCwicG9saWN5IjoiY29uc29sZUFkbWluIn0...
示例程序的命令行参数(见 flag 定义):
| 参数 | 默认值 | 说明 |
|---|---|---|
-sts-ep |
http://localhost:9000 |
STS 端点 |
-u / -p |
无(必填) | MinIO 用户名 / 密码 |
-d |
false | 仅打印生成的临时凭证 |
-e |
0 | 请求的凭证有效时长(Go duration,内部换算为 DurationSeconds) |
-b |
用户名 | 用临时凭证列出该桶的对象 |
-s |
无 | 会话策略 JSON 文件路径 |
其核心调用链是:
- 用
cr.STSAssumeRoleOptions填入用户名/密码/策略/时长,cr.NewSTSAssumeRole(stsEndpoint, stsOpts)创建 STS 身份提供者; li.Get()触发一次真实的 AssumeRole 请求并返回{AccessKeyID, SecretAccessKey, SessionToken};- 将同一凭证注入
minio.Options,即可像普通客户端一样调用ListObjects等对象 API——SDK 会自动用SessionToken参与签名。
这个示例同时展示了临时凭证的两个消费端:-d 模式只展示凭证;不带 -d 时会用临时凭证实际调用对象 API(列桶),证明凭证端到端可用。
七、适用前提与注意事项小结
基于文档与源码,使用 AssumeRole 前需确认:
- 请求者必须是普通 MinIO 用户:临时凭证(
IsTemp)和服务账号(IsServiceAccount)不能作为 AssumeRole 的签名方,请求头中也不能带 session token(checkAssumeRoleAuth); - 父用户必须有效且策略可解析:被禁用用户的策略查询会失败,请求返回
InvalidParameterValue; - 有效期双重约束:请求方的
DurationSeconds(900–31536000 秒)与服务端MINIO_STS_DURATION(若设置则覆盖请求值,默认 1 小时)共同决定最终exp(GetDefaultExpiration); - 会话策略只做减法:
Policy与用户既有策略取交集,且序列化后不得超过 2048 字节、Version字段必填; RoleArn/RoleSessionName无实际语义:这是为兼容 AWS 客户端而存在的占位参数,响应中AssumedRoleUser.Arn恒为空。
AssumeRole 是 MinIO STS 家族中实现最轻量的一个 action:它不依赖 OIDC、LDAP 或外部插件,任何已部署的多用户 MinIO 都能直接使用。若你的场景涉及 OIDC 登录换取凭证或 AD/LDAP 用户换取凭证,可进一步参考同目录的 web-identity.md 与 ldap.md,它们与 AssumeRole 共享同一套 sts-handlers.go 中的策略校验、有效期计算与临时用户注册机制。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00