MinIO STS 对接 Dex 实践:基于 OIDC 的 Web Identity 联邦认证与临时凭证完整指南
本篇围绕 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-app与secret: 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.idTokens:3h决定了 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)的声明名,如 policy、groups、name。该声明是生成临时凭证的强制项:缺失时 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
从源码看该程序的工作流程:
- 拉取发现文档:
parseDiscoveryDoc()(L78-L100)GETconfig-ep,解析出authorization_endpoint、token_endpoint、jwks_uri等端点; - 发起授权:访问
http://localhost:8080/时(L172-L183),若提供了-csec则调用config.AuthCodeURL(state)发起授权码流跳转;否则调用implicitFlowURL()(L113-L136)以response_type=id_token发起隐式流; - 回调换凭证:
/oauth2/callback处理函数(L185-L269)先校验state防 CSRF,随后用config.Exchange(ctx, code)换取 token 并取出id_token; - 调用 MinIO STS:通过
credentials.NewSTSWebIdentity(stsEndpoint, getWebTokenExpiry)构造 STS 凭据提供者(底层即AssumeRoleWithWebIdentityAPI 调用),再用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 后:
- 被重定向到 Dex 登录页 —— 点击 "Login with email",输入用户名密码:
username:
admin@example.compassword:password - 点击 "Grant access"(Dex 默认开启授权审批页,对应
dex.yaml中skipApprovalScreen: false); - 浏览器将显示 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-app、name: admin、sub、iss: 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)将 AssumeRoleWithWebIdentity、AssumeRoleWithClientGrants、AssumeRoleWithLDAPIdentity、AssumeRoleWithCertificate、AssumeRoleWithCustomToken 等动作分别挂载到 POST 路由上,均要求 Version=2011-06-15。AssumeRoleWithWebIdentity 与 AssumeRoleWithClientGrants 统一收敛到 AssumeRoleWithSSO 处理。
JWT 校验:AssumeRoleWithSSO(L373 起)核心是调用
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_openid 的 claim_name 配置(见 cmd/utils.go)。支持字符串、字符串数组或逗号分隔值——这也是 groups 声明可以天然映射多个组策略的原因。
临时用户身份与入库:
sub声明是强制的,缺失即报 "subclaim is mandatory"(L527-L531);- 临时凭证的
ParentUser取base64(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/dex,storage: sqlite3 |
dex.yaml |
| Dex 客户端注册 | staticClients:id=example-app,redirect http://localhost:8080/oauth2/callback |
dex.yaml |
| Dex 演示用户 | staticPasswords:admin@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_url、client_id、claim_name、scopes 等 |
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.md、ldap.md、assume-role.md。
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