mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-01 08:40:23 +00:00
e2583ddcd6
* 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>
220 lines
16 KiB
Markdown
220 lines
16 KiB
Markdown
# 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](oidc-provider-requirements.md). The [Keycloak](#keycloak) and [Authing](#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](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](oidc-provider-requirements.md#sts-workload-providers). 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](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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
2. Provider visibility: `curl -fsS https://rustfs.example.com/rustfs/admin/v3/oidc/providers | jq` lists the provider unless `hide_from_ui=on`.
|
|
|
|
3. 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.
|
|
|
|
4. 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):
|
|
|
|
```bash
|
|
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.
|