首页
/ MinIO STS 对接 Dex 实践:基于 OIDC 的 Web Identity 联邦认证与临时凭证完整指南

MinIO STS 对接 Dex 实践:基于 OIDC 的 Web Identity 联邦认证与临时凭证完整指南

2026-09-06 11:14:30作者:贡沫苏Truman

本篇围绕 MinIO 仓库中 Dex Quickstart Guide 展开,完整讲解如何将 Dex 作为 OpenID Connect(OIDC)身份提供方接入 MinIO,使终端用户通过 Dex 登录页完成认证后,经 STS 接口 AssumeRoleWithWebIdentity 换取 MinIO 临时凭证。读完本文,你将掌握 Dex 配置文件的逐项解读(含仓库内可直接使用的 dex.yaml 示例)、MinIO 侧 identity_openid 配置方法、样例客户端 web-identity.go 的运行流程,以及 MinIO STS 服务端对 JWT 声明(claim)的校验与策略映射源码实现。

为什么选择 Dex 作为 MinIO 的 OIDC 身份提供方

MinIO 的 STS(Security Token Service)是一种端点服务,允许客户端请求 MinIO 资源的临时凭证(详见 MinIO STS Quickstart Guide)。临时凭证相比长期管理凭证的优势在于:

  • 有效期短(几分钟到数小时均可配置),过期后 MinIO 不再认可,无需轮换或显式吊销;
  • 无需将长期凭证硬编码进应用,凭证在需要时动态生成;
  • 免去为每次访问预先定义静态凭证。

在 MinIO 支持的三种身份联邦方式中,与 Dex 相关的是 WebIdentity

认证方式 说明
WebIdentity 让任何 OIDC 兼容的 Web 身份提供方(Keycloak、Dex、Google 等)的用户请求临时凭证
AD/LDAP 使用 AD/LDAP 用户名和密码请求临时凭证
AssumeRole 使用 MinIO 用户的 AccessKey/SecretKey 请求临时凭证

Dex 本身是一个基于 OIDC 驱动应用认证的身份服务,它通过 "connectors"(连接器)作为通往其他身份提供方的门户:LDAP 服务器、SAML 提供方,或 GitHub、Google、Active Directory 等成熟身份提供方。客户端只需面向 Dex 编写一次认证逻辑,Dex 负责处理各后端的具体协议。这使得 Dex 成为把企业既有身份体系(例如 AD/LDAP)桥接到 MinIO 这类只理解 OIDC 的对象存储时的理想中间层。

整体请求链路为:

浏览器/客户端  ->  Dex (OIDC IdP, :5556)  ->  用户认证(mock/LDAP/SAML 等 connector)
     |
     v  获得 id_token (JWT)
MinIO STS (:9000)  ->  AssumeRoleWithWebIdentity  ->  校验 JWT + 提取 policy claim
     |
     v
临时凭证 (AccessKeyID / SecretAccessKey / SessionToken)

前置条件:安装并配置 Dex

按照 Dex 官方 Getting Started 指南安装 Dex 后,使用 MinIO 仓库自带的示例配置 docs/sts/dex.yaml 即可快速起步。该配置文件涵盖了本文所有演示步骤所需的全部设定,下面逐项解读:

# Dex 的基础路径与 OpenID Connect 服务的外部名称。
# 这是所有客户端必须用来引用 dex 的规范 URL。若提供 path,
# dex 的 HTTP 服务将监听在非根 URL 上。
issuer: http://127.0.0.1:5556/dex

# 存储配置决定 dex 在哪里保存状态。支持 SQL 方言与
# Kubernetes 第三方资源。
storage:
  type: sqlite3
  config:
    file: examples/dex.db

# HTTP 端点配置。
web:
  http: 0.0.0.0:5556
  # Uncomment for HTTPS options.
  # https: 127.0.0.1:5554
  # tlsCert: /etc/dex/tls.crt
  # tlsKey: /etc/dex/tls.key

  # 遥测配置
  telemetry:
    http: 0.0.0.0:5558

# 有效期时长配置(取消注释以启用)。
expiry:
  signingKeys: "3h"
  idTokens: "3h"

  # 日志选项
  logger:
    level: "debug"
    format: "text" # 也可以是 "json"

# 以下为默认值
oauth2:
  # 启用纯 Web 客户端的隐式流时使用 ["code", "token", "id_token"]
  responseTypes: [ "code", "token", "id_token" ] # 还允许 "token" 与 "id_token"
  # 默认情况下,Dex 会请求授权与应用共享数据
  skipApprovalScreen: false
  # 若只启用一种认证方式,默认行为是直接跳转。
  # 对已连接的 IdP,这会把浏览器从应用重定向到上游提供方
  # (例如 Google 登录页)
  alwaysShowLoginScreen: false
  # 取消注释 passwordConnector 以使用特定 connector 处理密码授权
  passwordConnector: local

# 不从外部存储读取,而是使用这份静态客户端列表。
#
# 若未选择此选项,客户端可通过 gRPC API 添加。
staticClients:
  - id: example-app
    redirectURIs:
      - 'http://localhost:8080/oauth2/callback'
    name: 'Example App'
    secret: ZXhhbXBsZS1hcHAtc2VjcmV0

connectors:
  - type: mockCallback
    id: mock
    name: Example

# 让 dex 维护一份可用于登录 dex 的密码列表。
enablePasswordDB: true

# 供最终用户登录的静态密码列表。在此声明后,dex
# 不会再到其底层存储中查找密码。
#
# 若未选择此选项,用户可通过 gRPC API 添加。
staticPasswords:
  - email: "admin@example.com"
    # "password" 字符串的 bcrypt 哈希
    hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
    username: "admin"
    userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"

关键配置要点:

  • issuer:MinIO 与客户端后续访问的规范 URL 前缀,本例为 http://127.0.0.1:5556/dex,因此 OIDC 发现文档地址为 http://127.0.0.1:5556/dex/.well-known/openid-configuration
  • staticClients:静态注册的 OIDC 客户端。id: example-appsecret: ZXhhbXBsZS1hcHAtc2VjcmV0(即 example-app-secret 的 base64 编码)正是后文运行样例客户端时 -cid/-csec 参数的取值;redirectURIs 中的 http://localhost:8080/oauth2/callback 对应样例客户端在 8080 端口暴露的回调地址;
  • connectors + staticPasswords:本例启用 mockCallback 连接器与本地密码库(admin@example.com / password),使演示无需真实的外部 IdP 即可完成完整登录流程;生产环境可替换为 LDAP、SAML、OAuth 等连接器,这正是 Dex 相对裸 OIDC 端点的核心价值;
  • expiry.idTokens3h 决定了 Dex 签发的 id_token(JWT)有效期。

启动 Dex 并确认监听正常

在 Dex 安装目录下执行:

~ ./bin/dex serve dex.yaml

启动成功的日志参考(与 dex.md 中给出的输出一致):

time="2020-07-12T20:45:50Z" level=info msg="config issuer: http://127.0.0.1:5556/dex"
time="2020-07-12T20:45:50Z" level=info msg="config storage: sqlite3"
time="2020-07-12T20:45:50Z" level=info msg="config static client: Example App"
time="2020-07-12T20:45:50Z" level=info msg="config connector: mock"
time="2020-07-12T20:45:50Z" level=info msg="config connector: local passwords enabled"
time="2020-07-12T20:45:50Z" level=info msg="config response types accepted: [code token id_token]"
time="2020-07-12T20:45:50Z" level=info msg="config using password grant connector: local"
time="2020-07-12T20:45:50Z" level=info msg="config signing keys expire after: 3h0m0s"
time="2020-07-12T20:45:50Z" level=info msg="config id tokens valid for: 3h0m0s"
time="2020-07-12T20:45:50Z" level=info msg="listening (http) on 0.0.0.0:5556"

从日志可以逐项核对:issuer 与 dex.yaml 中一致、静态客户端已加载、mock 连接器与本地密码库均已启用、监听地址为 0.0.0.0:5556

配置 MinIO 使用 Dex 作为 OIDC IdP

Dex 就绪后,通过两个环境变量把 MinIO 指向它并指定策略声明名:

~ export MINIO_IDENTITY_OPENID_CLAIM_NAME=name
~ export MINIO_IDENTITY_OPENID_CONFIG_URL=http://127.0.0.1:5556/dex/.well-known/openid-configuration
~ minio server ~/test

两个环境变量的含义(对应 identity_openid 配置子系统的键,可在 help.go 中查到完整定义):

环境变量 配置键 类型 说明
MINIO_IDENTITY_OPENID_CONFIG_URL config_url url OIDC 发现文档地址,如 https://accounts.google.com/.well-known/openid-configuration;MinIO 启动时会从中拉取 JWKS 公钥端点用于校验 id_token 签名
MINIO_IDENTITY_OPENID_CLAIM_NAME claim_name string JWT 中承载策略名(policy claim)的声明名,如 policygroupsname。该声明是生成临时凭证的强制项:缺失时 STS 会拒绝签发凭证(源码见下文)

此外,identity_openid 还支持以下常用键(见 internal/config/identity/openid/openid.go 中的键名常量与默认值定义,键名常量位于 L48-L58):

配置键 说明
client_id 应用的公开唯一标识符,如 Dex 中的 example-app
client_secret 客户端密钥(敏感字段)
role_policy 绑定到该客户端应用与 IdP 的 IAM 访问策略,如 app-bucket-write,app-bucket-list
scopes 逗号分隔的 OpenID scope 列表,缺省使用发现文档中声明的 scopes,如 email,admin
claim_userinfo 是否从 UserInfo 端点拉取声明(on/off
vendor 指定供应商类型以启用特定行为

这些环境变量等价于以下 mc 配置命令(参见 casdoor.md 中对 identity_openid 配置方式的说明):

~ mc admin config set myminio identity_openid \
    config_url="http://127.0.0.1:5556/dex/.well-known/openid-configuration" \
    claim_name="name"

说明:本仓库当前版本中 run-multi-site-oidc.sh 演示脚本同样使用 MINIO_IDENTITY_OPENID_CONFIG_URL="http://localhost:5556/dex/.well-known/openid-configuration" 指向本地 Dex,与本文 Dex 端口规划一致。

运行样例客户端 web-identity.go

MinIO 仓库提供了一个可直接运行的 Web Identity 样例客户端 docs/sts/web-identity.go(文件头带有 //go:build ignore,属于独立示例程序而非主包代码)。其命令行参数定义于 init()

参数 默认值 说明
-sts-ep http://localhost:9000 MinIO STS 端点
-config-ep Keycloak 示例地址 OIDC 发现文档端点,此处须指向 Dex
-cid 空(必填) OIDC Client ID
-csec OIDC Client Secret;为空时走隐式流(implicit flow),非空时走授权码流
-cscopes openid 逗号分隔的请求 scope 列表
-port 8080 样例客户端的 Web 服务监听端口

dex.md 的命令执行:

~ go run web-identity.go -cid example-app -csec ZXhhbXBsZS1hcHAtc2VjcmV0 \
     -config-ep http://127.0.0.1:5556/dex/.well-known/openid-configuration \
     -cscopes groups,openid,email,profile

从源码看该程序的工作流程:

  1. 拉取发现文档parseDiscoveryDoc()L78-L100)GET config-ep,解析出 authorization_endpointtoken_endpointjwks_uri 等端点;
  2. 发起授权:访问 http://localhost:8080/ 时(L172-L183),若提供了 -csec 则调用 config.AuthCodeURL(state) 发起授权码流跳转;否则调用 implicitFlowURL()L113-L136)以 response_type=id_token 发起隐式流;
  3. 回调换凭证/oauth2/callback 处理函数(L185-L269)先校验 state 防 CSRF,随后用 config.Exchange(ctx, code) 换取 token 并取出 id_token
  4. 调用 MinIO STS:通过 credentials.NewSTSWebIdentity(stsEndpoint, getWebTokenExpiry) 构造 STS 凭据提供者(底层即 AssumeRoleWithWebIdentity API 调用),再用 minio-go 客户端执行 ListBuckets 验证凭证可用,最终把发现的 bucket 列表与临时凭证以 JSON 输出到浏览器。

创建策略并授予权限

在用户登录换取凭证之前,需要先让凭证携带的策略存在于 MinIO。dex.md 演示为 admin 用户创建全访问策略:

~ mc admin policy create admin allaccess.json

allaccess.json 内容:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:*"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    }
  ]
}

注意策略名与 JWT claim 值的对应关系:MINIO_IDENTITY_OPENID_CLAIM_NAME=name 表示从 id_token 的 name 声明取值作为策略名。Dex 登录成功后签发的 token 中 name 声明为 admin,因此必须存在名为 admin 的策略(即上面 mc admin policy create admin ... 创建的策略),凭证才有访问权限。若 claim 值对应的策略不存在,STS 会直接报错 None of the given policies (...) are defined, credentials will not be generated

浏览器端验证:完整登录与凭证输出

访问 http://localhost:8080 后:

  1. 被重定向到 Dex 登录页 —— 点击 "Login with email",输入用户名密码:

    username: admin@example.com password: password

  2. 点击 "Grant access"(Dex 默认开启授权审批页,对应 dex.yamlskipApprovalScreen: false);
  3. 浏览器将显示 bucket 列表与从 MinIO 获取的临时凭证,输出形如:
{
 "buckets": [
  "dl.minio.equipment",
  "dl.minio.service-fulfillment",
  "testbucket"
 ],
 "credentials": {
  "AccessKeyID": "Q31CVS1PSCJ4OTK2YVEM",
  "SecretAccessKey": "rmDEOKARqKYmEyjWGhmhLpzcncyu7Jf8aZ9bjDic",
  "SessionToken": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJRMzFDVlMxUFNDSjRPVEsyWVZFTSIsImF0X2hhc2giOiI4amItZFE2OXRtZEVueUZaMUttNWhnIiwiYXVkIjoiZXhhbXBsZS1hcHAiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6IjE1OTQ2MDAxODIiLCJpYXQiOjE1OTQ1ODkzODQsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTU1Ni9kZXgiLCJuYW1lIjoiYWRtaW4iLCJzdWIiOiJDaVF3T0dFNE5qZzBZaTFrWWpnNExUUmlOek10T1RCaE9TMHpZMlF4TmpYeFpqVTBOallTQld4dlkyRnMifQ.nrbzIJz99Om7TvJ04jnSTmhvlM7aR9hMM1Aqjp2ONJ1UKYCvegBLrTu6cYR968_OpmnAGJ8vkd7sIjUjtR4zbw",
  "SignerType": 1
 }
}

至此,Dex IdP 与 MinIO 的对接配置成功完成。解码 SessionToken(JWT)可以看到其中携带了 Dex 签发的声明:aud: example-appname: adminsubiss: http://127.0.0.1:5556/dex 等,与 dex.yaml 中的客户端注册信息完全对应。

除样例客户端外,也可以直接使用 MinIO 控制台:在浏览器打开 MinIO 地址,点击 "Login with SSO",用户会被重定向到 Dex 登录页,登录成功后自动回到 MinIO 页面并以可见的 bucket/对象范围完成登录(流程见 sts/README.md 中 "Using MinIO Console" 一节)。

进阶:使用 groups 声明实现基于组的授权

原文档提示:Dex 支持通过外部连接器提供 groups,因此可以使用 groups 作为策略声明代替 name

将声明名改为 groups:

export MINIO_IDENTITY_OPENID_CLAIM_NAME=groups

然后为每个组在 MinIO 上创建同名的策略,例如组名为 engineering 时:

mc admin policy create myminio/ engineering group-access.json

这样,只要用户所属的某个组名与已创建的策略名匹配,其临时凭证就获得对应权限。相比按个人(name)授权,按组授权在用户频繁变动、外部目录(LDAP/AD)作为真实用户源的场景下维护成本更低。

源码纵深:MinIO STS 如何校验 JWT 并签发凭证

理解了上面的操作序列后,再看 MinIO 服务端 cmd/sts-handlers.go 的实现,整个安全边界就清晰了。

路由注册registerSTSRouter()L139-L189)将 AssumeRoleWithWebIdentityAssumeRoleWithClientGrantsAssumeRoleWithLDAPIdentityAssumeRoleWithCertificateAssumeRoleWithCustomToken 等动作分别挂载到 POST 路由上,均要求 Version=2011-06-15AssumeRoleWithWebIdentityAssumeRoleWithClientGrants 统一收敛到 AssumeRoleWithSSO 处理。

JWT 校验AssumeRoleWithSSOL373 起)核心是调用

if err := globalIAMSys.OpenIDConfig.Validate(r.Context(), roleArn, token, accessToken,
    r.Form.Get(stsDurationSeconds), claims); err != nil {

该调用使用 MinIO 启动时从 MINIO_IDENTITY_OPENID_CONFIG_URL 发现文档获取的 JWKS 公钥验证 id_token 签名,并校验 iss/aud/exp 等声明,同时把声明展开到 claims map 中。token 过期会返回 ErrTokenExpired,映射为 ErrSTSWebIdentityExpiredToken 错误响应。

策略声明提取(policy claim 强制项)

policySet, ok := policy.GetPoliciesFromClaims(claims, iamPolicyClaimNameOpenID())
policies := strings.Join(policySet.ToSlice(), ",")
if ok {
    policyName = globalIAMSys.CurrentPolicies(policies)
}

if newGlobalAuthZPluginFn() == nil {
    if !ok {
        writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue,
            fmt.Errorf("%s claim missing from the JWT token, credentials will not be generated", iamPolicyClaimNameOpenID()))
        return
    } else if policyName == "" {
        writeSTSErrorResponse(ctx, w, ErrSTSInvalidParameterValue,
            fmt.Errorf("None of the given policies (`%s`) are defined, credentials will not be generated", policies))
        return
    }
}
claims[iamPolicyClaimNameOpenID()] = policyName

L466-L494)这正是 STS README 中 "policy claim is mandatory" 警告的实现:声明缺失或声明值(策略名)在 MinIO 中未定义,都会拒绝签发凭证。iamPolicyClaimNameOpenID() 读取的就是 identity_openidclaim_name 配置(见 cmd/utils.go)。支持字符串、字符串数组或逗号分隔值——这也是 groups 声明可以天然映射多个组策略的原因。

临时用户身份与入库

  • sub 声明是强制的,缺失即报 "sub claim is mandatory"(L527-L531);
  • 临时凭证的 ParentUserbase64(sha256("openid:" + sub + ":" + iss))L538-L548),源码注释解释了原因:该值会作为盘上策略映射文件的文件名,必须只含合法文件名字符且长度有界;
  • 最后通过 globalIAMSys.SetTempUser(ctx, cred.AccessKey, cred, policyName) 将临时用户与策略名绑定,返回 AssumeRoleWithWebIdentityResponse,其中 SubjectFromWebIdentityToken 即为 token 中的 sub 值(L613-L622)。

[sts-handlers_test.go](https://gitcode.com/GitHub_Trending/mi/minio/blob/7aac2a2c5b7c882e68c1ce017d8256be2feea27f/cmd/sts-handlers_test.go?utm_source=gitcode_repo_files) 中的测试(如基于 identity_openid 多配置的用例)覆盖了上述校验路径,可作为行为验证的参考。

小结与关键参数速查

环节 关键配置/命令 出处
Dex issuer 与存储 issuer: http://127.0.0.1:5556/dexstorage: sqlite3 dex.yaml
Dex 客户端注册 staticClientsid=example-app,redirect http://localhost:8080/oauth2/callback dex.yaml
Dex 演示用户 staticPasswordsadmin@example.com / password(bcrypt) dex.yaml
启动 Dex ./bin/dex serve dex.yaml dex.md
MinIO 指向 Dex MINIO_IDENTITY_OPENID_CONFIG_URL=.../.well-known/openid-configuration dex.md
策略声明名 MINIO_IDENTITY_OPENID_CLAIM_NAME=name(或 groups dex.md
样例客户端 go run web-identity.go -cid ... -csec ... -cscopes ... web-identity.go
策略创建 mc admin policy create admin allaccess.json dex.md
组级授权 MINIO_IDENTITY_OPENID_CLAIM_NAME=groups + 组同名策略 dex.md
STS 服务端校验 OpenIDConfig.Validate + policy claim 提取 + SetTempUser sts-handlers.go
identity_openid 配置键 config_urlclient_idclaim_namescopes help.go

生产部署时需要注意:将 dex.yaml 中的 staticPasswords/mockCallback 替换为真实连接器(LDAP、SAML、企业 OIDC);Dex 启用 HTTPS(web.https + 证书)并以 HTTPS 提供 issuer;MinIO 侧的 client_secret 等敏感配置优先通过 mc admin config set 写入配置存储而非仅依赖环境变量;临时凭证有效期受 MINIO_IDENTITY_OPENID_* 相关过期配置与 STS DurationSeconds 参数共同约束。完整的 STS 背景与 JWT 声明规范可继续阅读 MinIO STS Quickstart Guide,其他 IdP 的同类指南见 keycloak.mdldap.mdassume-role.md

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