Files
rustfs/docs/operations/oidc-console-integration.md
InitCool e2583ddcd6 feat(oidc): make the Console session duration configurable (#8568)
* feat(oidc): make the Console session duration configurable

The OIDC callback hard-coded the Console credential lifetime at 3600
seconds, so a Console session always ended after about an hour.
RUSTFS_CONSOLE_AUTH_TIMEOUT, the documented knob next to this behaviour,
is the Console HTTP request timeout and cannot extend a session.

Resolve the lifetime from the process-level
RUSTFS_CONSOLE_OIDC_SESSION_DURATION instead and forward it to
complete_authorization_code(). Unset, unparseable, zero, and negative
values keep DEFAULT_CONSOLE_OIDC_SESSION_DURATION_SECS (3600), so
existing deployments keep the previous behaviour and a misconfigured
variable can never issue an already expired credential. Values above
CONSOLE_OIDC_SESSION_DURATION_MAX_SECS (7 days) are clamped to that
maximum with a warning, which also caps how far an operator can stretch a
credential by accident.

Keep the Console logout token for that same lifetime. OidcStateStore
pinned the logout cache to a hard-coded hour, so a session configured to
last longer than that dropped its logout token while the credential was
still valid: RP-initiated logout could no longer resolve, and the browser
was sent back to the Console login page with the IdP session still
active. The logout cache now derives its TTL from the same
console_session_duration_secs(), which applies the same clamp, and the
callback reads its lifetime from that one function instead of a local
copy, so the credential lifetime and the logout-token retention cannot
drift apart. Add tests covering the default hour, an extended session,
the maximum, and out-of-range values.

The variable is deliberately not part of ENV_IDENTITY_OPENID_KEYS: that
list is suffix-scanned per provider, and this setting applies to every
provider. It has no identity_openid KVS equivalent and needs a restart,
like the other process-level settings in the same document.

Document the setting in the login flow, the process-level table, and the
troubleshooting table, and clarify that RUSTFS_CONSOLE_AUTH_TIMEOUT is a
request timeout rather than a session lifetime.

* test(ecstore): retain offline marker fixtures across reconnect

Keep simulated drive handles attached and reject I/O until explicit rejoin.
Attempt reconnect in the current-marker case so the former CI race is
deterministic while retaining physical cleanup and historical-data checks.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* fix(oidc): align session diagnostics and lifetime regression tests

Use structured IAM diagnostics for clamping and move configuration tests
to their owning crate. Cover duration boundaries, logout retention and
signed credential expiration through the federation binding.

Co-Authored-By: heihutu <heihutu@gmail.com>
Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

---------

Co-authored-by: Chris <anzhengchao@gmail.com>
Co-authored-by: Hauser <housemecn@gmail.com>
Co-authored-by: heihutu <heihutu@gmail.com>
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
2026-10-19 06:52:05 +09:00

16 KiB

OIDC Console integration

Use this when: connecting RustFS Console login to an OpenID Connect provider (Keycloak, Authing, or any standards-compliant IdP), or debugging an OIDC redirect, token, or policy-mapping failure. Source of truth: crates/config/src/constants/oidc.rs (provider keys and RUSTFS_IDENTITY_OPENID_*), crates/iam/src/oidc/config.rs (per-provider env suffixes), crates/iam/src/oidc/provider.rs (discovery and JWKS), crates/iam/src/oidc/runtime.rs (PKCE and token validation), rustfs/src/admin/handlers/oidc.rs (authorize/callback handlers), crates/config/src/constants/app.rs (ENV_RUSTFS_BROWSER_REDIRECT_URL), crates/utils/src/egress.rs (ENV_OUTBOUND_ALLOW_ORIGINS), crates/policy/src/policy/policy.rs (built-in policies).

The RustFS side is vendor-neutral and is described once; what RustFS requires from any provider is tabulated in oidc-provider-requirements.md. The Keycloak and Authing sections contain only IdP-side steps and vendor caveats. Examples use provider id default and public origin https://rustfs.example.com.

Integration model

RustFS requires a standards-compliant OpenID Connect provider: discovery at <issuer>/.well-known/openid-configuration, authorization and token endpoints, a JWKS, and an authorization-code flow that returns an id_token. RustFS never calls a vendor's authorization API; access is decided by RustFS IAM policies after claim mapping. Protocol requirements for IdP vendors are collected in oidc-provider-requirements.md.

Login flow:

  1. The browser opens https://rustfs.example.com/rustfs/admin/v3/oidc/authorize/<provider_id>.
  2. RustFS creates state, nonce, and a PKCE S256 challenge and redirects to the IdP.
  3. The IdP redirects back to /rustfs/admin/v3/oidc/callback/<provider_id>?code=...&state=....
  4. RustFS exchanges the code at the token endpoint, sending client_id and client_secret in the request body (client_secret_post) together with the PKCE verifier.
  5. RustFS validates the ID token signature (JWKS), issuer, audience, expiry, and nonce.
  6. RustFS maps claim values to policy names and issues STS credentials to the Console with the lifetime set by RUSTFS_CONSOLE_OIDC_SESSION_DURATION (default 3600 seconds, one hour).

In-flight state and PKCE verifiers are node-local: the authorize and callback requests must reach the same RustFS node.

Configuration keys

Every provider key can be set as RUSTFS_IDENTITY_OPENID_<KEY> in the process environment or as identity_openid <key>=<value> through mc admin config set. Names are constants in crates/config/src/constants/oidc.rs.

Provider key Environment variable Purpose
enable RUSTFS_IDENTITY_OPENID_ENABLE on loads the provider.
config_url RUSTFS_IDENTITY_OPENID_CONFIG_URL Issuer URL used for discovery. A trailing /.well-known/openid-configuration is stripped; any other .well-known path is rejected.
issuer RUSTFS_IDENTITY_OPENID_ISSUER Expected iss when it differs from config_url (internal discovery URL, public token issuer).
client_id, client_secret RUSTFS_IDENTITY_OPENID_CLIENT_ID, RUSTFS_IDENTITY_OPENID_CLIENT_SECRET Confidential client credentials.
scopes RUSTFS_IDENTITY_OPENID_SCOPES Comma-separated; openid is required.
other_audiences RUSTFS_IDENTITY_OPENID_OTHER_AUDIENCES Additional accepted aud values.
redirect_uri RUSTFS_IDENTITY_OPENID_REDIRECT_URI Callback URL sent to the IdP; must equal the URL registered there.
redirect_uri_dynamic RUSTFS_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC on derives the callback from request headers. Keep off behind proxies.
claim_name, claim_prefix RUSTFS_IDENTITY_OPENID_CLAIM_NAME, RUSTFS_IDENTITY_OPENID_CLAIM_PREFIX Policy claim name and a fixed string prepended to each value. claim_prefix is not a mapping table.
groups_claim, roles_claim RUSTFS_IDENTITY_OPENID_GROUPS_CLAIM, RUSTFS_IDENTITY_OPENID_ROLES_CLAIM Flat top-level array claims whose values are RustFS policy names.
email_claim, username_claim RUSTFS_IDENTITY_OPENID_EMAIL_CLAIM, RUSTFS_IDENTITY_OPENID_USERNAME_CLAIM Identity claims shown in the Console.
role_policy RUSTFS_IDENTITY_OPENID_ROLE_POLICY One fixed policy for every login from this provider. Connectivity testing only.
display_name RUSTFS_IDENTITY_OPENID_DISPLAY_NAME Login button label.
hide_from_ui RUSTFS_IDENTITY_OPENID_HIDE_FROM_UI Hides the provider from /oidc/providers. Required (on) for STS workload issuers whose discovery document omits authorization_endpoint; see workload provider requirements. Complete hidden providers still support direct Console login.

Process-level settings (environment only, never suffixed per provider):

Variable Purpose
RUSTFS_BROWSER_REDIRECT_URL Public browser origin used for callback generation, Console success redirects, and logout fallback.
RUSTFS_OUTBOUND_ALLOW_ORIGINS Exact scheme://host[:port] origins RustFS may contact for discovery, JWKS, and token requests when the IdP resolves to a private, loopback, or container-network address. See outbound-connection-policy.md.
RUSTFS_CONSOLE_OIDC_SESSION_DURATION Credential lifetime in seconds for Console credentials issued by this callback, for every provider. Default 3600. Values that are missing, not a positive integer, or zero keep the default; values above 604800 (7 days) are clamped to 604800 with a warning. The same value also sets how long the Console logout token stays valid, so RP-initiated logout keeps working for the whole session. Note that RUSTFS_CONSOLE_AUTH_TIMEOUT does not control this: it is the Console HTTP request timeout.

Named providers: to use provider id <id>, suffix every provider env var with _<id> (for example RUSTFS_IDENTITY_OPENID_CLIENT_ID_keycloak) and register the callback /rustfs/admin/v3/oidc/callback/<id>. Suffix scanning is parse_single_provider in crates/iam/src/oidc/config.rs.

Restart RustFS after changing any of these settings.

Environment example

export RUSTFS_BROWSER_REDIRECT_URL="https://rustfs.example.com"

export RUSTFS_IDENTITY_OPENID_ENABLE=on
export RUSTFS_IDENTITY_OPENID_CONFIG_URL="<ISSUER>"
export RUSTFS_IDENTITY_OPENID_CLIENT_ID="<CLIENT_ID>"
export RUSTFS_IDENTITY_OPENID_CLIENT_SECRET="<CLIENT_SECRET>"
export RUSTFS_IDENTITY_OPENID_SCOPES="openid,profile,email"
export RUSTFS_IDENTITY_OPENID_REDIRECT_URI="https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default"
export RUSTFS_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC=off
export RUSTFS_IDENTITY_OPENID_DISPLAY_NAME="<IdP name>"
export RUSTFS_IDENTITY_OPENID_GROUPS_CLAIM="groups"
export RUSTFS_IDENTITY_OPENID_ROLES_CLAIM="roles"
export RUSTFS_IDENTITY_OPENID_EMAIL_CLAIM="email"
export RUSTFS_IDENTITY_OPENID_USERNAME_CLAIM="preferred_username"

The same keys through admin config:

mc admin config set rustfs identity_openid \
  enable=on config_url="<ISSUER>" client_id="<CLIENT_ID>" client_secret="<CLIENT_SECRET>" \
  scopes="openid,profile,email" \
  redirect_uri="https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default" \
  redirect_uri_dynamic=off display_name="<IdP name>" \
  groups_claim="groups" roles_claim="roles" email_claim="email" username_claim="preferred_username"
mc admin service restart rustfs

RUSTFS_BROWSER_REDIRECT_URL is not an identity_openid key; it must still be set in the process environment.

Redirect URL priority

  1. Provider redirect_uri, when set, is the callback URL sent to the IdP.
  2. RUSTFS_BROWSER_REDIRECT_URL, when set, is the public origin for callback generation when no provider redirect_uri exists, and for Console success and logout fallback redirects.
  3. Request headers (Host, X-Forwarded-Proto) are used only when redirect_uri_dynamic=on and no browser redirect URL is configured.

Behind a reverse proxy or load balancer, set RUSTFS_BROWSER_REDIRECT_URL and keep session affinity for the authorize and callback requests.

Policy mapping

Claim values are used verbatim as policy names (after claim_prefix, if any). Names must satisfy is_safe_claim_policy_name in crates/iam/src/sys.rs: ASCII letters, digits, _, -, :, . only, so a value containing / (for example Keycloak's full group path /consoleAdmin) never matches. Built-in policies:

Policy Grants
consoleAdmin Full Console, admin, KMS, and S3 access.
readwrite S3 read/write.
readonly S3 read-only.
writeonly S3 write-only.
diagnostics Diagnostic admin access.

For first-contact testing only, RUSTFS_IDENTITY_OPENID_ROLE_POLICY=consoleAdmin grants every login full access; remove it before production.

Validation

  1. Discovery:
curl -fsS "<ISSUER>/.well-known/openid-configuration" | jq '{issuer, authorization_endpoint, token_endpoint, jwks_uri, code_challenge_methods_supported, token_endpoint_auth_methods_supported, scopes_supported}'

issuer must equal RUSTFS_IDENTITY_OPENID_ISSUER when set, otherwise the issuer derived from RUSTFS_IDENTITY_OPENID_CONFIG_URL; code_challenge_methods_supported must include S256; token_endpoint_auth_methods_supported must include client_secret_post; scopes_supported must include every configured scope.

  1. Provider visibility: curl -fsS https://rustfs.example.com/rustfs/admin/v3/oidc/providers | jq lists the provider unless hide_from_ui=on.

  2. Browser login: open https://rustfs.example.com/rustfs/admin/v3/oidc/authorize/default. Expect a redirect to the IdP, sign-in, a redirect to /rustfs/admin/v3/oidc/callback/default?code=...&state=..., and then the Console with the mapped permissions.

  3. ID token claims (decode the token after a test login): iss matches the issuer, aud includes the client id, email and preferred_username are present when configured, groups or roles is a flat array of policy names.

Troubleshooting

Symptom Common cause Fix
/oidc/providers does not list the provider provider failed to load, or RustFS was not restarted Check env/admin config and restart RustFS.
Provider or login button missing; startup logs OIDC provider discovery blocked by outbound policy IdP origin is private/internal and not allowlisted Add the exact origin to RUSTFS_OUTBOUND_ALLOW_ORIGINS on every node and restart.
IdP reports a redirect mismatch (invalid redirect_uri) registered callback differs from RustFS redirect_uri Use the exact /rustfs/admin/v3/oidc/callback/<provider_id> URL on both sides.
Callback reports missing code or state proxy dropped the query string Preserve the full callback URL and query string.
Token exchange fails wrong secret, or the IdP rejects request-body client authentication Confirm the client is confidential and accepts client_secret_post.
No id_token in the token response openid scope missing or a non-OIDC OAuth flow Add openid; use the authorization-code flow.
ID token verification fails issuer, audience, algorithm, or JWKS mismatch Compare discovery metadata with CONFIG_URL/ISSUER/CLIENT_ID; prefer RS256.
Login succeeds, access denied no claim value matches a policy name Emit groups or roles as a flat array equal to policy names; check for / prefixes.
Console redirects to an internal host RUSTFS_BROWSER_REDIRECT_URL unset or proxy headers wrong Set RUSTFS_BROWSER_REDIRECT_URL to the public origin.
Invalid or expired OIDC state callback reached a different node Configure load-balancer session affinity for authorize and callback.
Console session expires after about an hour callback issues credentials with the default lifetime Set RUSTFS_CONSOLE_OIDC_SESSION_DURATION to the desired lifetime in seconds (max 604800, 7 days) and restart RustFS. RUSTFS_CONSOLE_AUTH_TIMEOUT is a request timeout and does not extend the session.

Production checklist

  • RustFS and the IdP use HTTPS.
  • The IdP registers the exact callback URL (no wildcard) and RUSTFS_IDENTITY_OPENID_REDIRECT_URI matches it.
  • RUSTFS_BROWSER_REDIRECT_URL is the public browser origin.
  • PKCE S256 is allowed or required at the IdP.
  • ID tokens carry groups or roles values equal to RustFS policy names.
  • role_policy is not used as a permanent shortcut.
  • The load balancer preserves query strings and pins authorize/callback to one node.
  • Internal IdP origins are listed exactly in RUSTFS_OUTBOUND_ALLOW_ORIGINS on every node.

Keycloak

Value Example
Realm rustfs
Issuer (config_url) https://keycloak.example.com/realms/rustfs
Discovery URL https://keycloak.example.com/realms/rustfs/.well-known/openid-configuration
Client id rustfs-console
Scopes openid,profile,email
Groups claim groups (flat array)

Client setup in the Keycloak Admin Console:

  1. Create or select the realm and confirm discovery returns issuer equal to https://keycloak.example.com/realms/rustfs.
  2. Clients → create: Client type = OpenID Connect, Client ID = rustfs-console.
  3. Enable Client authentication and Standard flow; disable Implicit flow, Direct access grants, and Service accounts roles.
  4. Valid redirect URIs = https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default; Web origins = https://rustfs.example.com.
  5. Proof Key for Code Exchange Code Challenge Method = S256.
  6. Save and copy the secret from Credentials. Do not apply a client policy that disables client_secret_post.

Group mapper (a Group Membership mapper in the client's dedicated scope):

Mapper field Value
Name rustfs-groups
Token Claim Name groups
Full group path Off (a leading / breaks policy matching)
Add to ID token / access token / userinfo On
Multivalued On

Create Keycloak groups named after RustFS policies (consoleAdmin, readonly, ...) and add users to them.

Roles instead of groups: assign realm or client roles named after policies, add a User Realm Role or User Client Role mapper that emits a flat top-level roles claim, and set RUSTFS_IDENTITY_OPENID_ROLES_CLAIM=roles. RustFS does not read Keycloak's nested realm_access.roles claim.

Internal discovery URL with a public issuer (for example in-cluster Keycloak on Kubernetes):

export RUSTFS_IDENTITY_OPENID_CONFIG_URL="http://keycloak.keycloak.svc.cluster.local:8080/realms/rustfs"
export RUSTFS_IDENTITY_OPENID_ISSUER="https://keycloak.example.com/realms/rustfs"
export RUSTFS_OUTBOUND_ALLOW_ORIGINS="http://keycloak.keycloak.svc.cluster.local:8080"

Discovery and issuer-relative JWKS requests use the CONFIG_URL base; iss validation uses ISSUER. The allowlist entry is the origin only (no realm or discovery path) and is read at startup on every node. Prefer HTTPS with a trusted CA for the internal URL: discovery and JWKS define the token-signing trust root, so plain HTTP is acceptable only where DNS and traffic cannot be tampered with.

Authing

Value Example Note
Application domain https://example.authing.cn From the Authing application page.
Issuer (config_url) https://example.authing.cn/oidc Tenants differ (/oidc, /oauth/oidc): copy the issuer from the console and confirm discovery returns the same issuer.
App ID / App Secret <AUTHING_APP_ID> / <AUTHING_APP_SECRET> RustFS client_id / client_secret.
Scopes openid,profile,email,roles roles is needed when Authing emits role claims.
Roles claim roles Set RUSTFS_IDENTITY_OPENID_ROLES_CLAIM=roles.

Application settings in the Authing console:

Setting Value
Protocol OpenID Connect
Grant type / response type Authorization Code / code
Token endpoint authentication client_secret_post
PKCE allow or require S256
ID token signing algorithm RS256
Redirect URL https://rustfs.example.com/rustfs/admin/v3/oidc/callback/default (exact)

Assign Authing roles whose names equal RustFS policy names; a test user with role consoleAdmin should produce "roles": ["consoleAdmin"] in the ID token. claim_prefix only prepends a fixed string, so keep role values equal to policy names unless policies with that prefix already exist.