首页
/ MinIO STS AssumeRole 深度解析:临时凭证 API 的规范、实战与源码实现

MinIO STS AssumeRole 深度解析:临时凭证 API 的规范、实战与源码实现

2026-09-06 10:26:27作者:齐添朝

本文以 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 官方文档给出的两个典型动机:

  1. 免去多部件上传的预签名难题:各客户端 SDK 的多部件上传流程复杂(含大量容错处理),而通用 SDK 并不支持"逐 URL 预签名"的多部件上传。持有临时凭证后,SDK 可以原生地完成整个 Multipart 流程,无需自己为每个 part 重新发明签名逻辑。
  2. 简化前缀级上传:客户端拿到会话凭证后,可以自行上传整个目录/前缀下的文件,服务端应用不必为每个文件生成并下发一条预签名 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-Typeapplication/x-www-form-urlencodedAuthorization 头为 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. 默认 1 小时:未设置环境变量、也未传 DurationSeconds 时,返回 time.Hour,与文档描述一致;
  2. MINIO_STS_DURATION 环境变量优先:设置了该变量(Go duration 格式)时,它直接覆盖请求方传入的 DurationSeconds,并且同样受 900–31536000 秒的范围约束。这意味着管理员可以在服务端一刀切地限制所有 STS 会话的长度,客户端无法申请更长;
  3. 越界即报错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 = 2048cmd/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、校验 VersionAction=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 文件路径

其核心调用链是:

  1. cr.STSAssumeRoleOptions 填入用户名/密码/策略/时长,cr.NewSTSAssumeRole(stsEndpoint, stsOpts) 创建 STS 身份提供者;
  2. li.Get() 触发一次真实的 AssumeRole 请求并返回 {AccessKeyID, SecretAccessKey, SessionToken}
  3. 将同一凭证注入 minio.Options,即可像普通客户端一样调用 ListObjects 等对象 API——SDK 会自动用 SessionToken 参与签名。

这个示例同时展示了临时凭证的两个消费端:-d 模式只展示凭证;不带 -d 时会用临时凭证实际调用对象 API(列桶),证明凭证端到端可用。

七、适用前提与注意事项小结

基于文档与源码,使用 AssumeRole 前需确认:

  1. 请求者必须是普通 MinIO 用户:临时凭证(IsTemp)和服务账号(IsServiceAccount)不能作为 AssumeRole 的签名方,请求头中也不能带 session token(checkAssumeRoleAuth);
  2. 父用户必须有效且策略可解析:被禁用用户的策略查询会失败,请求返回 InvalidParameterValue
  3. 有效期双重约束:请求方的 DurationSeconds(900–31536000 秒)与服务端 MINIO_STS_DURATION(若设置则覆盖请求值,默认 1 小时)共同决定最终 expGetDefaultExpiration);
  4. 会话策略只做减法Policy 与用户既有策略取交集,且序列化后不得超过 2048 字节、Version 字段必填;
  5. RoleArn / RoleSessionName 无实际语义:这是为兼容 AWS 客户端而存在的占位参数,响应中 AssumedRoleUser.Arn 恒为空。

AssumeRole 是 MinIO STS 家族中实现最轻量的一个 action:它不依赖 OIDC、LDAP 或外部插件,任何已部署的多用户 MinIO 都能直接使用。若你的场景涉及 OIDC 登录换取凭证或 AD/LDAP 用户换取凭证,可进一步参考同目录的 web-identity.mdldap.md,它们与 AssumeRole 共享同一套 sts-handlers.go 中的策略校验、有效期计算与临时用户注册机制。

登录后查看全文
热门项目推荐
相关项目推荐