Authentication & Tokens
Rise both issues its own JWTs and accepts JWTs from external identity
providers. This page is the operator’s map of that token model: what each token
is for, which configuration and endpoints govern it, and the security
implications you need to plan for. For the exact key names and YAML, see the
Configuration Guide; for the internal
algorithm/claim disambiguation rules, see the engineering reference in
crates/rise-backend-auth/README.md.
At a glance
Section titled “At a glance”| Token | Direction | Algorithm | Audience (aud) | Verified by | Governed by |
|---|---|---|---|---|---|
| Session | Issued | HS256 | Rise public_url | Rise API middleware | server.jwt_signing_secret, server.jwt_expiry_seconds |
| Access | Issued | HS256 | Rise public_url | Rise API middleware | server.auth_token_max_ttl_seconds, auth.allow_raw_external_tokens |
| Identity | Issued | RS256 | Rise public_url, or a caller-named external verifier | Rise API middleware (re-resolved against the resource store per request), or the external verifier via Rise JWKS | server.rs256_private_key_pem, server.auth_token_max_ttl_seconds; trust policies are resources |
| Ingress | Issued | RS256 | Project URL | Nginx/ingress via Rise JWKS | server.rs256_private_key_pem |
| Workload identity | Issued | RS256 | Caller-supplied (e.g. sts.amazonaws.com) | External system via Rise JWKS | server.rs256_private_key_pem, deployment_controller.identity_token_ttl_seconds |
| User login (OIDC) | Accepted | per IdP | — | Rise (JWKS of auth.issuer) | auth.issuer, auth.client_id, auth.client_secret |
| Service account | Accepted | RS256 (JWKS) | project-scoped | Rise (JWKS of the SA issuer) | per-project (CLI/API managed) |
| Controller | Accepted | RS256 (JWKS) | per policy | Rise (JWKS of the trust policy’s issuer) | Controller + ControllerTrustPolicy resources |
Tokens Rise issues
Section titled “Tokens Rise issues”Rise acts as an OIDC issuer for the RS256 tokens below. External systems verify them against Rise’s public key, published at:
- JWKS:
GET {public_url}/api/v1/auth/jwks - OIDC discovery:
GET {public_url}/.well-known/openid-configuration(served at the root, per the OIDC spec)
Session (HS256)
Section titled “Session (HS256)”The token a user (or the CLI) holds after logging in; it authenticates requests
to the Rise API and UI. It is symmetric (HS256), signed with
server.jwt_signing_secret (base64, ≥ 32 bytes), scoped to the Rise
public_url audience, and carries the claims listed in server.jwt_claims.
Lifetime is server.jwt_expiry_seconds (default 24h). Session tokens are an
internal concern — they are never verified outside Rise.
A session names the user’s Rise User resource: its header typ is
rise-session+jwt, sub is the canonical user:<name>, rise_uid is the
User’s UID, and rise_identity_uid is the UserIdentity the login came
through. rise_client records what the session was issued to: browser
(the web UI’s sign-in) or cli (rise login, with either flow). Every
request re-resolves the User and identity:
spec.active: falseon the User, or deleting it, ends every session issued for it;spec.active: falseon one UserIdentity, or deleting it, ends the sessions minted through that identity, and leaves the User’s other logins alone.
email stays in the token for the typed APIs. A session issued before this
shape (default typ, the IdP’s sub, no rise_uid) keeps working on the typed
APIs until it expires, and ends early once any login has mapped its IdP sub
and that identity or its User is disabled; the generic resource API refuses it
with 401 — log in again. If the store cannot be reached to re-check a session,
the request fails with 500 rather than logging the user out.
Access (HS256) — token exchange
Section titled “Access (HS256) — token exchange”A short-lived, Rise-issued token that encodes a fully-resolved principal (a
service account or a controller). It is minted by the RFC 8693 token-exchange
endpoint and is symmetric (HS256, aud = Rise public_url), so — like Session
tokens — it is verified only inside Rise and never by external parties. It is
distinguished from a Session token by its JWT header typ (rise-access+jwt).
Why it exists. Without it, a CI service account presents its external OIDC token on every request and Rise re-resolves the service account per request (issuer lookups, JWKS verification, and claim matching in the hot path). With token exchange, the caller resolves once, up front, and every subsequent request checks only that the embedded service account still stands as the token says.
Exchange endpoint: POST /api/v1/auth/token (public — the subject token is
the credential). It follows RFC 8693:
// request{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "<external OIDC JWT>", "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", "identity": "my-project+0@sa.rise.local" // the identity to assume (a service // account's email); omit for a controller}// response{ "access_token": "<rise HS256 jwt>", "token_type": "Bearer", "issued_token_type": "urn:ietf:params:oauth:token-type:jwt", "expires_in": 600}identity is a selector (which service account to assume); the subject token
is the proof. The exchange looks the SA up by that email, verifies the inner
OIDC token exactly as the accept paths below do (issuer guard → SA issuer match →
JWKS signature + exp → the SA’s claim matching), then mints the access token.
With an identity it resolves a project service account (its project is
derived from the SA); without one, a controller identity. Every
identity-dependent failure (unknown email, not an SA, wrong issuer, claim
mismatch) returns the same coarse invalid_grant so SA emails cannot be
enumerated. Lifetime is
server.auth_token_max_ttl_seconds (default 600s). Each request re-reads the
service account the token names: deleting the SA, or changing its environment
restriction, rejects its outstanding tokens immediately. A change to the SA’s
issuer or claims applies to the next exchange.
This STS-style endpoint is distinct from the OIDC
token_endpoint(the authorization-code exchange at/api/v1/auth/code/exchange); the discovery document continues to advertise the latter.
This endpoint resolves the typed service accounts and configured controllers
that the project APIs use today. Its resource-model successor is the token
subresource below; the two coexist until the typed-object migration moves
service accounts onto the resource store.
Identity (RS256) — the token subresource
Section titled “Identity (RS256) — the token subresource”A short-lived, Rise-issued token whose subject is a ServiceAccount or
Controller resource of the generic resource API (serviceaccount:<org>/<name>
or controller:<name>), carrying that resource’s UID as rise_uid. It is minted
only by the target’s own token subresource —
POST /api/v1/resources/rise.dev/v1alpha1/serviceaccounts/{org}/{name}/token or
…/controllers/{name}/token — in one of two disjoint modes: workload token
exchange, where an external OIDC JWT in the body is validated against the
ServiceAccountTrustPolicy / ControllerTrustPolicy children of that exact
target; or delegated issuance, where an already-authenticated Rise principal
holding (create, <kind>, token) on the target mints for it.
It is always RS256, signed with the same key as ingress and workload tokens and
distinguished from them by its header typ (rise-identity+jwt). The request
may name an audience: omitted, the token is for Rise’s own API, which is the
only audience the API accepts; any other value mints the same token for that
external verifier, which checks it against the JWKS above. One key and one
token kind serve both, which is why the key is load-bearing for the resource
API as well.
What it carries and what it does not. Identity and an optional ceiling,
never a snapshot of grants. Rise re-resolves (sub, rise_uid) to one live
resource on every request and evaluates the identity’s current RBAC,
intersected with the token’s RFC 9396 authorization_details if present. So:
- deleting the target, or a draining Organization above it, fails every outstanding token immediately — there is nothing to revoke;
- recreating the same name under a new UID does not revive old tokens;
- narrowing the target’s Roles takes effect on the next request;
- revoking the caller’s token-create grant stops new issuance only.
Delegated tokens record their delegators in a bounded act chain (at most four),
for audit; actor data never grants access. Lifetime is capped by
server.auth_token_max_ttl_seconds, the platform-global maximum shared with
Access tokens. Trust policies are ordinary governed resources: they hold public
matching configuration (issuer, aud, further string claims with * globs),
never keys, and may not name Rise’s own issuer. Every failure of a workload
exchange after the route is entered is one coarse 401, so the route confirms
nothing about which identities or policies exist. The request/response shape and
the authorization rules are specified in the
resource HTTP API.
Migrating off raw tokens (auth.allow_raw_external_tokens). Defaults to
true: a service account may still present its raw external OIDC token directly
to project-scoped endpoints (the legacy per-request path), which Rise resolves as
before. Set it to false to require pre-exchange — callers must obtain an access
token from /api/v1/auth/token first. While it is true, every accepted
raw-token request (after JWKS validation) emits one metric-shaped tracing
event (target=rise::deprecation, metric=raw_external_token) carrying the
validated issuer/sub (and aud) — aggregate it in your log pipeline (count,
group by issuer/sub) to see which workload identities still need migrating,
not just their issuer. Leaving it true forfeits the security benefits above.
Defaults to false starting in 0.25.0; migrate CI to pre-exchange before
upgrading.
Ingress (RS256)
Section titled “Ingress (RS256)”Issued for private project access enforcement at the ingress layer (see
Private Project Authentication). It is
asymmetric (RS256) so the ingress can verify it via Rise’s JWKS without holding a
shared secret, and its audience is the project URL. Minted by GET /api/v1/auth/ingress.
Workload identity (RS256)
Section titled “Workload identity (RS256)”Issued to deployed apps so they can federate identity to external systems
(AWS STS, GCP Workload Identity Federation, HashiCorp Vault, …) without
long-lived secrets. The subject describes the Rise identity
(rise:proj:<project>:env:<environment>), the audience is supplied per request
by the caller, and the token is signed with the same RS256 key as ingress
tokens — so the JWKS/discovery endpoints above already cover verification.
Two paths produce these tokens:
- The controller auto-mints them and projects them into pods under
/var/run/secrets/rise/tokens/. Lifetime isdeployment_controller.identity_token_ttl_seconds(default 3600s); the controller re-mints once a token passes half its lifetime. - Apps may also call
POST /api/v1/identity/tokendirectly, exchanging their deployment bootstrap credential (Authorization: Bearer <credential>) for a token with a requested audience. Its lifetime is capped by the sameidentity_token_ttl_seconds— one cap for every workload identity token — and defaults to it. On ECS the identity sidecar writes the token files through this endpoint.
The RS256 key is operationally load-bearing
Section titled “The RS256 key is operationally load-bearing”server.rs256_private_key_pem signs ingress, workload, and identity tokens and
backs the JWKS. If you do not configure it, Rise generates a fresh key pair
on every start — which silently invalidates all previously issued ingress,
workload, and identity tokens, differs between replicas, and rotates the JWKS
out from under any external verifier.
- Always set
rs256_private_key_pem(and optionallyrs256_public_key_pem, otherwise derived) in any non-ephemeral deployment. - Treat it as a secret; store it via your secret manager, not in plain config.
- Rotating it invalidates outstanding ingress/workload tokens and changes the published JWKS — coordinate with anything that has cached the keys.
Tokens Rise accepts
Section titled “Tokens Rise accepts”When Rise receives a bearer token whose issuer is not Rise itself, it
discovers that issuer’s JWKS via {issuer}/.well-known/openid-configuration
(SSRF-validated) and verifies the RS256 signature, iss, and exp before
applying per-identity claim constraints.
User login (OIDC)
Section titled “User login (OIDC)”Interactive login federates to your IdP via auth.issuer / auth.client_id /
auth.client_secret / auth.scopes. On success Rise mints a Session token
(above). This is how operators wire Rise to Dex, Okta, Entra ID, Google, etc.
Browser sign-in uses a short-lived, host-only nonce cookie in addition
to PKCE and single-use OAuth state. The central callback requires the proof before
exchanging the code. App sign-in establishes an independent proof on the app host,
then visits Rise’s central host before the IdP redirect. The final
/.rise/auth/complete step requires the app-host proof and the transaction’s exact
app origin. A callback or completion URL alone cannot establish a browser session.
Rise must successfully consume the transaction in PostgreSQL before returning a
session cookie or app completion redirect; a storage failure refuses the response.
Each host reuses one nonce per stage across concurrent sign-ins and refreshes its
ten-minute expiry on each start. Completing one transaction leaves the nonce
available to other pending transactions. Browsers must accept the proof cookies;
expired or interrupted sign-ins restart from the sign-in page. Simultaneous first
starts without a nonce can require restarting one sign-in.
Each login is resolved to a Rise User resource by the ID token’s exact
(iss, sub) pair, recorded in a UserIdentity child of the User (ADR-0001 §1):
- First login. No live
UserIdentityholds the pair, so Rise creates a newUsernamedu-<ulid>and its firstUserIdentityin one transaction. Concurrent first logins converge on one User. The new User holds no grants of its own. - Returning login. The pair resolves to its User. Email never links
accounts: a different
subwith the same email is a different User. - Disabled. An inactive
UserIdentity, or an active one under an inactive User, refuses the login (403); it is never treated as unknown. Deleting theUserIdentityinstead only unlinks it — the next login provisions a fresh User with a new UID — so disable withspec.active: false.
auth.issuer must be spelled exactly as the IdP stamps iss — with or
without its trailing slash, as the IdP does — and otherwise canonical (lower-case
host, no default port), or the server refuses to start. The IdP must issue a
stable sub.
There is deliberately no way to trade an ID token for a session outside these
login flows: they receive the ID token from the IdP over the back channel,
bound to the login by PKCE, state, and nonce, while an endpoint accepting
presented ID tokens would honor any token the IdP issues for Rise’s client,
from any flow.
Device login (rise login --device)
Section titled “Device login (rise login --device)”Rise is the RFC 8628 authorization server itself; the IdP needs no device grant.
- The CLI calls
POST /api/v1/auth/authorize {"flow": "device"}and gets a device code, a user code (XXXX-XXXX, from the RFC’s consonant alphabet) andverification_uri_complete = {public_url}/device?user_code=…. The row lands indevice_authorizations, holding only a SHA-256 of the device code; it expires after 10 minutes. The endpoint is unauthenticated, so starts count againstserver.oauth_rate_limit(per client IP, and in their own bucket). - The user opens
/devicein the Rise UI, signs in the usual way, and approves (POST /api/v1/auth/device/approve) or denies. Approval requires a browser session (rise_client: browser) that names its User and identity and was issued at most 10 minutes ago. Anything else gets401, and the page sends the user through/auth/signin/startfirst. The approval records that session’s User UID andUserIdentityUID. - The CLI polls
POST /api/v1/auth/device/exchange. Answers are the RFC’sauthorization_pending,slow_down(the interval grows by 5s),access_deniedandexpired_token, plusserver_errorfor a transient failure. An approved row is deleted as it is redeemed, so it yields one session. The User and identity are re-resolved first, and the session is minted for exactly that pair. Disabling either one ends the device session, just like the browser session that approved it.
No IdP token is presented at any step. Every device session traces back to a
recent browser login that was bound by PKCE, state and nonce. Device
sessions are cli sessions, and so are those from rise login’s code flow,
so a CLI session can never approve a device login. That means it cannot mint
its own successor and outlive the IdP login.
Service accounts (CI/CD)
Section titled “Service accounts (CI/CD)”Project-scoped identities for automation (e.g. pipelines) that present an OIDC JWT from a trusted CI issuer. They are managed per project through the CLI/API rather than static config; Rise validates the token against the issuer’s JWKS and the service account’s configured claim constraints. See the service-account workflow in the Rise user documentation.
Controllers
Section titled “Controllers”Trusted external controllers authenticate with OIDC JWTs matched against live
ControllerTrustPolicy resources rather than static config. An operator
creates a root-scoped Controller resource for the identity, then one or more
ControllerTrustPolicy children under it, each declaring an issuer and
required claims (aud is mandatory; add sub/scope/etc. as needed; *
wildcards are supported). Several trust policies matching the same
Controller is an ordinary match — a Controller may accept more than one
issuer or claim shape; policies matching different Controllers make a token
ambiguous and the request is refused with 409. spec.issuer must equal the
token’s iss byte-for-byte, matching UserIdentity.
A Controller authenticates against the generic resource API (
/api/v1/resources) as the ordinary principalcontroller:<name>. What it may do is decided by RBAC alone: grant itupdateon thestatusandfinalizerssubresources of a kind (or any other verb) through aPlatformRoleBindingnaming that subject. See Controller authorization for the full model and a worked example. A controller may also exchange its OIDC JWT once, at its owntokensubresource, for an identity token that carries the samecontroller:<name>principal.
Security checklist
Section titled “Security checklist”-
server.jwt_signing_secretis a unique, ≥ 32-byte base64 secret (rotating it invalidates all sessions). -
server.rs256_private_key_pemis set and persisted (see above). -
server.cookie_secure: trueand anhttps://public_urlin production. - Every accepted issuer (
auth.issuer, service accounts, controllers) is one you control or explicitly trust; constrainaud(andsubwhere possible). - Workload
audvalues are the specific external systems you intend (avoid broad audiences);deployment_controller.identity_token_ttl_secondsis as short as your federation tolerates.