Skip to content
rise.Operator docs
GitHub

Changelog

Release notes and upgrade instructions. Filter changes by impact and deployment backend. Upcoming changes are merged to develop; planned changes are not yet merged.

Unreleased

Merged · not released

Changes merged to develop that are not yet in a full release. Release candidates may include these changes.

BreakingAll backendsChange

Path-based app URLs are no longer supported.

Breaking for installs whose ingress URL templates contain a path (e.g. production_ingress_url_template: "apps.example.com/{project_name}"); only the Kubernetes backend ever honored one. Every *_ingress_url_template must now be a bare hostname pattern — no scheme, port or path — and the backend refuses to start otherwise. Apps sharing a host shared its cookies and origin, and on Docker and ECS their routers collided outright. Switch to subdomain templates such as {project_name}.apps.example.com with a wildcard DNS record and certificate before upgrading; app URLs change accordingly.

ChangeKubernetes

Kubernetes no longer routes a group that has no host of its own.

A deployment group with neither a deployment-group host nor an environment host — in practice the default group, with no staging_ingress_url_template, in a project that has no production environment mapped to it — used to get an Ingress on the production host on Kubernetes only. It now gets none, as on Docker and ECS, and Rise no longer advertises a URL for it: its primary_url is empty and its containers get no RISE_APP_URL. Point an environment at the group, or configure a staging template, to route it.

BreakingAll backendsChange

Preview hostnames join the project with --.

Breaking. The staging and environment templates must now join {project_name} to {deployment_group} / {environment} with exactly --, and the backend refuses to start otherwise (full rule: Ingress hostname templates). A single hyphen let one project’s preview host equal another’s: project a with group b-c and project a-b with group c shared a-b-c.

  • The config/production.yaml and Helm CI defaults change from {project_name}-{deployment_group} / {project_name}-{environment} to {project_name}--{deployment_group} / {project_name}--{environment}. Override RISE_STAGING_INGRESS_URL_TEMPLATE / RISE_ENVIRONMENT_INGRESS_URL_TEMPLATE accordingly if you set them.
  • Without a staging template, a group’s host is now <project>--<group>.<domain> instead of <project>-<group>.<domain>.
  • Preview and environment URLs change accordingly; production URLs and custom domains do not.
  • New project names may not contain --. Existing ones keep working, but can still collide: find them with SELECT name FROM projects WHERE name LIKE '%--%';. Docker and ECS route a host claimed by two projects to the older one only.
Action requiredKubernetes · Amazon ECSChange

The Kubernetes and ECS backends are now licensed under BUSL-1.1.

Action required for Kubernetes and ECS operators. The Kubernetes and Amazon ECS backends and the Helm chart move from MIT OR Apache-2.0 to the Business Source License 1.1. Each release converts back to MIT OR Apache-2.0 four years after it is published. The CLI, the server core, the Docker backend and the Terraform modules stay MIT OR Apache-2.0. Running the Kubernetes or ECS backend in production from this release on requires a commercial license (contact niklas@rosenstein.email); non-profits, and development, testing and evaluation, do not. The license explicitly permits production use of the server image with neither controller enabled, but license scanners will still report the image as BUSL-1.1. Nothing changes at runtime. See LICENSING.md.

BreakingKubernetesChange

Kubernetes 1.32 is the minimum supported version.

Breaking. Rise’s Kubernetes client now targets the 1.32 API, and the Helm chart declares kubeVersion: ">=1.32.0-0", so helm install/helm upgrade fails on older clusters. Upgrade the cluster to 1.32 or newer before upgrading Rise. The minimum follows the oldest version Rise’s Kubernetes client types support and will keep rising with them.

Action requiredAll backendsChange

Every deployment belongs to an environment.

Action required. A migration makes deployments.environment_id required. Deployments without one (from before environments existed, or left behind by a deleted environment) are assigned to the environment whose primary group they ran in, else to production, else to the project’s oldest environment; a project with no environments gets production first. Deleting an environment now deletes its deployments and their history, and the API refuses with 409 while any of them is not stopped. Before upgrading, stop the deployments of environments you plan to delete if you want that history gone, and expect environment-less deployments to show up under an environment afterwards. The workload identity sub no longer carries env:<null>.

Action requiredDocker · Amazon ECSChange

Docker/ECS route every active deployment group, each on its own hosts.

Action required. The Traefik HTTP-provider snapshot used to route only the newest active deployment group of each project, so production and older previews returned Traefik’s 404 page not found (#548). Every group is now routed. A custom domain is attached only to the router of its environment’s primary deployment group, never to preview groups, and domains that match an ingress URL template are no longer routed. Kubernetes already behaved this way. Before upgrading, look for Ignoring custom domain that conflicts with project default domain pattern warnings: those domains stop being served on Docker/ECS. A deployment whose stored container/route data does not decode now fails the whole snapshot, and Traefik keeps its last good config until the row is fixed.

BreakingAll backendsChange

CLI browser login requires numeric IPv4 loopback callbacks.

Breaking. The CLI listens and redirects on 127.0.0.1, trying ports 8765, 8766, then 8767. Before upgrading the CLI, register these exact URLs on the identity provider’s Rise client (Keycloak: Valid redirect URIs): http://127.0.0.1:8765/callback, http://127.0.0.1:8766/callback, and http://127.0.0.1:8767/callback. The bundled development, Helm, CI, and ECS Dex configurations allow only these numeric CLI callbacks. CLIs that redirect to localhost cannot use browser login with those configurations; upgrade the CLI alongside the configuration, or use rise login --device.

Config changeAll backendsChange

rise login --device is served by Rise, not the IdP.

Config change. The device code is now confirmed on Rise’s own /device page, after a fresh browser sign-in. The IdP no longer needs to support, or enable, the OAuth device grant, and a device-grant setting on Rise’s IdP client can be removed. The bundled Dex config drops urn:ietf:params:oauth:grant-type:device_code. Existing CLIs keep working: the endpoints and responses are unchanged. Migration 20260924000001 adds the device_authorizations table.

Action requiredAll backendsChange

User logins resolve to User resources; sessions carry rise_uid.

Action required. Every interactive login (browser, CLI code and device flows, app ingress sign-in) now resolves the ID token’s exact (iss, sub) to a rise.dev/User through a UserIdentity, creating both on a first login. New session tokens carry typ: rise-session+jwt, sub = user:<name> and the User’s rise_uid, and are re-resolved on every request. The generic resource API authorizes that User — user:<name> in a binding names it.

  • Log in again to use the resource API. A session issued before the upgrade keeps working on the typed APIs (projects, teams, deployments) until it expires, but /api/v1/resources answers it with 401.
  • auth.issuer must be spelled exactly as your IdP stamps iss (trailing slash included if the IdP uses one) and otherwise canonical, or the server refuses to start. The same rule now admits trailing-slash issuers in ServiceAccountTrustPolicy / ControllerTrustPolicy.
  • Disable a user with spec.active: false on their User (every login and session ends) or on one UserIdentity (that login and the sessions it minted). Deleting a UserIdentity ends its sessions too but merely unlinks it: the next login through it provisions a new User. A session from before the upgrade ends early too, once a login has mapped its IdP sub.
  • Migration 20260922000001 adds users.resource_user_uid, linking each typed user to the User resource it last logged in as.
  • Operator standing is unchanged: still auth.operator_users / auth.operator_idp_groups, matched against the login’s typed user.
BreakingAll backendsChange

Controllers are RBAC principals; auth.controllers and the status/finalizer allowlist are removed.

Breaking. A controller now authenticates by matching a live ControllerTrustPolicy resource beneath a live root Controller resource, not the auth.controllers[] config list — delete that key, it is silently ignored if left in place. What a controller may do is decided by RBAC alone: grant it update on a kind’s status and finalizers subresources (or any other verb) through an ordinary PlatformRoleBinding naming its controller:<name> subject. There is no more per-collection allowlist: ResourceDefinition.spec.allowedStatusControllerIds is gone from the API (a client that still sends it gets 400) and is stripped from stored definitions by migration.

Action required if you configured auth.controllers or allowedStatusControllerIds:

  • Create a Controller resource and one ControllerTrustPolicy child per accepted issuer/claim shape, replacing each auth.controllers[] entry.
  • Grant each controller a PlatformRoleBinding for the kinds and subresources it needs — an empty grant now means the controller reaches nothing, not that it reaches whatever allowedStatusControllerIds listed.
  • If a controller is mid-reconcile with finalizers outstanding, grant its binding before removing the allowlist entry (or grant it in the same change), so it can still clear its own finalizers rather than needing an operator’s operator_update_finalizers recovery path.
  • Re-exchange any cached access tokens: an exchanged controller token’s sub is now controller:<name> (was rise:ctrl:<id>) and its principal claim carries {kind: controller, name, uid} instead of a bare identity_id.
  • The controller’s writer key under status.controllers is now its resource name (a DNS label), not the old config id (a DNS subdomain); update finalizer prefixes (<name>/<reason>) to match.

See Authentication & Tokens — Controllers and Controller authorization for the full setup.

FeatureAll backends

Generic resource API: the token subresource on ServiceAccount and Controller.

No config change; one new route shape. POST /api/v1/resources/rise.dev/v1alpha1/serviceaccounts/{org}/{name}/token and …/controllers/{name}/token issue Rise identity tokens, either by exchanging an external OIDC JWT against that resource’s trust-policy children or by delegation from a principal holding (create, <kind>, token) on it. A credential-less POST to such a path now reaches the resource API rather than being refused by the authentication layer; it is rate-limited through the OAuth limiter, and anything that is not a registered token route stays 401. Identity tokens are accepted only by the generic resource API and are re-resolved against the live resource on every request. They are RS256-signed with server.rs256_private_key_pem, the key behind the published JWKS, so a request may name an external audience and hand the token to that verifier; this also means the key is now load-bearing for the resource API itself — an unset key is regenerated on every start and differs per replica, which invalidates identity tokens on restart and across replicas exactly as it already does ingress and workload tokens. The typed-table exchange at POST /api/v1/auth/token and the CLI are unchanged. See Authentication & Tokens.

BreakingAmazon ECSChange

ECS: workload identity.

Breaking. ECS deployments now get the workload identity files every other backend delivers — the bootstrap credential for rise identity token, and the [identity].audiences token files — so a deployment declaring [identity] is no longer rejected. Every new ECS task runs an identity sidecar (rise identity agent, from the new ghcr.io/rise-deploy/rise-cli image) next to the app; see ECS › Workload identity.

  • Action required: set the sidecar image. The server refuses to start without deployment_controller.identity_agent_image (RISE_ECS_IDENTITY_AGENT_IMAGE), normally ghcr.io/rise-deploy/rise-cli:<the same version>. The rise-ecs module requires rise_cli_image_tag (or rise_cli_image_ref), exactly like rise_image_tag. The workload execution role must be able to pull it.

  • Upgrading rolls every running ECS service once. The first reconcile tick provisions each running deployment’s credential and adds the sidecar, which registers a new task-definition revision; ECS replaces the tasks as a rolling update, without a routing gap. Expect a burst of RegisterTaskDefinition calls (throttled to 1/s) on large installs.

  • Tasks must reach the token endpoint, the public URL by default. Set identity_exchange_url (RISE_ECS_IDENTITY_EXCHANGE_URL) if they cannot.

  • identity_token_ttl_seconds is now env-driven on ECS (RISE_IDENTITY_TOKEN_TTL_SECONDS), and rise-ecs exposes it along with identity_exchange_url.

Config changeAll backendsChange

The workload identity token-exchange endpoint has its own rate limiter.

Config change. POST /api/v1/identity/token no longer shares the OAuth limiter. A request with a valid bootstrap credential counts against that deployment’s budget only; the per-IP budget applies only to credentials that match no deployment. Configure it under server.workload_token_rate_limit (per_deployment_max/per_deployment_window_secs, default 500 per 60 s; rejected_per_ip_max/rejected_per_ip_window_secs, default 50 per 60 s). server.oauth_rate_limit no longer applies to this endpoint.

BreakingAll backendsChange

One lifetime cap for workload identity tokens; server.workload_token_max_ttl_seconds is removed.

Breaking. Tokens from the token-exchange endpoint (POST /api/v1/identity/token, rise identity token) are now capped by deployment_controller.identity_token_ttl_seconds — the lifetime of the auto-minted [identity] token files — on every backend. The default lifetime of an exchanged token rises from 900 s to 3600 s, matching the token files a workload could already read; a request’s ttl_seconds still lowers it. The server refuses to start while server.workload_token_max_ttl_seconds is set, rather than silently lengthening tokens you had capped: remove the key and set identity_token_ttl_seconds to the lifetime you want.

Config changeAmazon ECSChange

ECS: a capacity setting, and service network configuration now converges.

Config change. deployment_controller.capacity selects where workload tasks run — fargate (the default, and what every existing install keeps doing) or ec2, which places them on the container instances of a cluster you already run. Networking is awsvpc on both, so subnets stays required; on ec2, enable ENI trunking on the instances, because each task takes its own ENI and the per-instance attachment limit — not CPU or memory — is what caps tasks per host. assign_public_ip is unavailable there — ECS refuses it on EC2 capacity, so the pair is rejected at startup and those subnets need a NAT gateway or VPC endpoints instead. Rise does not provision the instances, an Auto Scaling group, or a capacity provider, and its own control plane still runs on Fargate. On ec2, cpu/memory are declared exactly as requested instead of being rounded up to Fargate’s size table. An unrecognised value is refused at startup rather than defaulted.

The behaviour change to know about is separate from the setting. The ECS reconciler previously compared only a service’s task-definition hash and desired count, so an edit to subnets, security_groups or assign_public_ip never reached a running service — it landed only if something else happened to trigger a task-definition update, and never on a scale change. Those are now compared against what ECS reports and applied when they differ. If your running services were created with a configuration you have since changed, the first reconcile tick after upgrading will apply it, which ECS performs as a rolling replacement, one service at a time. Check for the difference before upgrading if that matters:

aws ecs describe-services --cluster "$CLUSTER" --services "$SERVICE" \
  --query 'services[].networkConfiguration.awsvpcConfiguration'

Changing capacity on an existing install does not move running services; ECS cannot. Each project picks it up on its next deployment, and until then the mismatch is logged once per service per tick.

Action requiredAmazon ECSChange

Terraform requirements and write-only secret state for rise-ecs.

The module requires Terraform 1.11+ and AWS provider 6.50+. Its managed Secrets Manager versions use write-only values at their stable resource addresses. The first plan against state created by an earlier module version still refreshes the readable attributes; use terraform plan -refresh=false when the plan identity intentionally cannot call secretsmanager:GetSecretValue. After the first apply, normal refresh uses ListSecretVersionIds.

Config changeAll backendsChange

Config change — registry.registry_host for ECR.

Overrides the host images are pushed to and deployed from (default <account_id>.dkr.ecr.<region>.amazonaws.com), for FIPS or dual-stack ECR endpoints and ECR-compatible emulators. RISE_ECR_REGISTRY_HOST on ECS; ecr_registry_host in rise-ecs. Unset keeps the current behavior.

BreakingAmazon ECSChange

Breaking (release candidates) — rise-ecs takes route53_zone, not route53_zone_id.

It takes the zone as an object, so a zone created in the same configuration works in one apply instead of failing the plan with “Invalid count argument”. Replace route53_zone_id = "Z..." with route53_zone = { zone_id = "Z..." }, or pass the zone resource or data source itself. The records keep their addresses; nothing is recreated.

ChangeAmazon ECS

rise-ecs secret versions stop changing on every plan.

Each secret version’s secret_string_wo_version was a hash too large for the AWS provider to store exactly, so every terraform plan replaced all four secret versions. The next apply updates each one a final time, with the same value; after that they stay put. Nothing to do.

Action requiredAmazon ECSChange

ECS runtime log access names the exact log group.

rise-aws requires ecs_log_group_name whenever enable_ecs = true, and grants FilterLogEvents/StartLiveTail on the wildcard-suffixed and bare log-group ARNs those APIs require. Set it to the same value as rise-ecs.log_group_name.

ChangeAmazon ECS

Rise Traefik reports ECS container health.

The task definition runs Traefik’s health command against its dedicated ping entrypoint, so ECS reports HEALTHY or UNHEALTHY instead of UNKNOWN.

Config changeAll backendsChange

Config change — login-time IdP group prefixes.

auth.idp_group_sync_prefixes optionally limits which canonical group names are mirrored into Rise teams. An empty list preserves synchronization of all canonical names. Invalid claim values are skipped, and excluded memberships are removed on the user’s next login. ECS and Docker installs can supply the prefixes as a comma-separated RISE_IDP_GROUP_SYNC_PREFIXES value.

FeatureAll backends

Snowflake private-key authentication accepts unencrypted PEM keys.

Rise converts unencrypted PKCS#8 and RSA PKCS#1 keys to encrypted PKCS#8 in memory for the connector. The generated passphrase is not persisted.

ChangeAll backends

The Pods tab is gone, and a deployment’s history is now an event log.

Every backend used to write a Kubernetes-shaped pod_status snapshot into controller_metadata on each reconcile, and the UI read into its keys. That made a controller’s internal note a public interface, and let two sources disagree about the same rollout. Deployments now record what happened as append-only events, read by the Timeline tab and the log console’s rail.

What this means in practice:

  • Deployments created before the upgrade have no events, so their Timeline reads “No events recorded” and their rail has nothing to draw. Their logs, status and everything else are unaffected. New deployments record from the moment they are created.
  • Per-replica detail is not yet recorded on any backend. Container start, exit and restart used to be visible in the Pods tab on Kubernetes and are currently visible nowhere. This is a gap being closed, not a backend difference — see the deployment backends matrix.
  • A “Controller” tab replaces it, rendering controller_metadata verbatim for introspection. It appears only when a backend writes something there, which today is ECS alone (its task-definition hash). Nothing should read into its keys: the shape belongs to whichever controller wrote it and carries no compatibility promise.
  • The migration removes the now-unwritten pod_status and health keys from existing rows. It rewrites every deployment row that has them, so on a large install expect the migration to take proportionally longer; it runs inside the usual startup transaction.
Config changeAll backendsChange

Config change — deployment history retention.

A new deployment_retention section bounds how much history Rise keeps.

  • max_events_per_deployment (default 1000) caps the events kept for any one deployment. A normal deployment records about six for its whole life, so the cap only engages for a deployment that oscillates between Healthy and Unhealthy, which would otherwise accumulate two rows per flap forever. A deployment’s first event is always kept, so its timeline still shows where it began.
  • delete_aged_deployments (default false) deletes finished deployments older than max_deployment_age_days (default 90). Off by default and irreversible: deleting a deployment takes its event log and its environment-variable snapshot with it, which is what makes it re-deployable. Turn it on only if you want that.
  • keep_primary_deployments_per_environment (default 10) protects each environment’s most recent finished deployments in its primary deployment group from the age rule, so rollback targets survive. The active deployment and anything still running are never deleted, whatever the settings say.

Both passes run hourly on the elected leader. Deletion is batched, so the first run after enabling it works through a backlog over several hours rather than in one transaction.

ChangeDocker

Docker installs restart the containers of non-public projects once, on the first reconcile after upgrade.

The Traefik router’s forwardAuth middleware was referenced as {router}-auth@docker. Naming a provider is wrong for any other: on ECS that reference resolves to nothing, Traefik logs middleware ... does not exist, and every request to a private project 404s while public projects route normally. The reference is now unqualified, which resolves within whichever provider read the labels.

Docker’s behaviour is unchanged — same provider, same middleware — but the label’s value feeds the container recreate signature, so each affected container is recreated once to pick it up. Only projects whose access class is Authenticated or Member carry the label at all; public projects have no middleware and are untouched. No action is required.

ChangeKubernetes

Kubernetes installs restart every pod once, on the first reconcile after upgrade.

Pods now also carry a rise.dev/project-uuid label alongside the existing rise.dev/project (name) label — the project’s immutable identity, for the same bookkeeping vocabulary the Docker and ECS backends already use. The label lands in the Deployment’s pod template, so every Deployment gets a new ReplicaSet and a one-time rolling restart to pick it up. This label is purely informational on Kubernetes today (queryable via kubectl get pods -l rise.dev/project-uuid=...); it does not by itself make a project rename safe there, since the RiseProject CRD’s own identity is still derived from the project name. No action is required beyond expecting the restart.

Action requiredAll backendsChange

Action required if you granted generic resource API access by adding people to auth.operator_users — the API is now authorized, not operator-gated.

/api/v1/resources no longer refuses everyone but operators. Every request is authorized by the ADR-0001 engine against the resource it names, and authorization-changing writes additionally pass the write-time grant gate inside a SERIALIZABLE transaction.

Operators are unaffected: an operator expands to system:operators, whose seeded system-admin binding allows every verb on every kind and subresource, and an operator’s request ignores every Deny. Nothing an operator could do before is refused now.

What changed for everyone else is that they are evaluated rather than refused outright. Shipped policy grants a non-operator nothing, so an install that has authored no bindings sees no access change — but if you were relying on “non-operator ⇒ 403” as the whole authorization story, that is no longer what the code says. Who reaches the evaluation at all is unchanged: the resource routes still sit behind the platform-access middleware, so auth.platform_access.policy: restrictive keeps its allowlist in front of everything below. Under the default allow_all that is every authenticated user. Two visible differences even without any binding:

  • A collection listing by a caller with no list grant returns an empty 200, not a 403. This is deliberate existence masking: a 403 would confirm the scope is populated. Naming one resource exactly is masked the same way: a caller with no get on it receives a 404, on every verb. A caller who can read the resource still gets a 403 naming the verb they are short.
  • Which collections exist is now visible to any authenticated caller, since the path is classified before any per-resource decision. What a collection contains is authorized per item.

Behaviour changes on the request/response surface:

  • metadata.effectiveLabels is present on every resource response, resolved live from the ancestor chain (nearest value wins per key).
  • A write returns the object at the granularity the caller may read. A caller holding (update, Kind, status) but not get gets the projected metadata shape back, not the spec — a write verb is not a read grant.
  • A main-resource PUT may no longer change metadata.finalizers; carrying the stored list back unchanged is fine, and changing them is the finalizers subresource’s separate grant. A create carrying a reserved system.rise.dev/* finalizer is rejected.
  • Changing a ResourceDefinition’s allowedStatusControllerIds now requires operator standing: it grants controllers status and finalizer writes outside the authorization model, so it is not something an ordinary update on the definition should confer.
  • A listing under an ancestor that does not exist returns an empty collection rather than 404, so the ancestor path is not enumerable by name. Item paths and creates under a missing ancestor still fail.
  • Changing the metadata.ownerReferences set is now authorized. Attaching or removing a reference requires use on the owner; attaching additionally requires delete on the dependent when it already exists, and setting blockOwnerDeletion: true requires delete on the owner, since that flag holds the owner’s own deletion open. Removing a reference whose owner is already gone or draining is ungated, so a dependent is never frozen behind an owner nobody can revive. Re-sending already-stored references is unaffected. Note that the shipped resource-owner role does not include use, so owning a resource does not by itself let you make it the owner of another — author a binding that grants use where you need that.
  • deletion-blockers filters its blockers per item on get and reports a hiddenBlockers count for those the caller cannot read. That count is a deliberate disclosure: it moves as resources the caller cannot see are created and deleted, so grant the subresource on that basis.
  • A refused authorization-changing write says less than it did. The recipient and the domain are named only when they came from the request; what the recipient would have gained is never named, because the gate compares their whole effective policy over the domain and a witness can come from any binding delivering policy to them. The full comparison is in the rise::audit resource.grant_gate record.
  • Attaching a metadata.ownerReferences entry to a Role, RoleBinding, PlatformRole, or PlatformRoleBinding now passes the grant gate as though the row were being deleted, and deleting an Organization passes it for every Role and RoleBinding beneath it. Both were routes around the gate: the cascade tombstones the row, and a tombstoned binding stops applying immediately, so removing a Deny this way was ungated. An operator is unaffected; a non-operator who could delete an Organization containing policy they cannot lift is now refused.
  • Creating a rise.dev/User with spec.active true (the default) passes the grant gate as an activation, matching the existing gate on flipping active from false to true. Policy binds to the User name, so recreating a name that stale bindings or GroupMembership markers still refer to makes that policy reachable again. Both gates now measure the name’s Group ties as well as the bindings naming it directly, so reactivating an offboarded identity requires holding whatever its Groups grant.
  • Security fix, outside the resource API. When the IdP takes over a team that already existed in Rise — sync_user_groups on login, and the Entra sync — every pre-existing membership is now removed, not just the owners. An IdP-managed team’s membership grants operator, admin, and platform access by group (auth.operator_idp_groups and friends), and team names are first-come, first-served while auth.allow_team_creation is on (the default): a user who created a team named after a privileged IdP group and listed themselves in it kept that membership through the takeover and inherited the group’s authority. Operator impact: if you pre-created teams that later became IdP-managed and relied on their Rise-side memberships, those members lose their group-derived roles until their next login, when the IdP re-asserts them. Members the IdP does not assert are not restored — that is the point. The takeover and the team-membership API now take a row lock on the team, so a membership write cannot slip into the window between the purge and its commit.
  • Deleting an Organization with more than 64 Role/RoleBinding resources beneath it is refused with 409 for a non-operator: each one has to be weighed against the writer’s authority, and more than that cannot be done in one transaction. Remove them first, or have an operator perform the delete.
  • A write response for a caller holding a write verb and neither get nor list is now apiVersion, kind, and metadata.name only — previously it carried labels and inherited effectiveLabels, which are org-wide and are what a list grant pays for.
  • Every 404 from these routes now carries the same body, {"error": "resource not found"}. Clients that matched on the old wording (“resource ‘x’ not found”, “parent path segment not found”) need updating. Authorization also runs before the request body is inspected, so a PUT with a malformed body against a resource the caller cannot read returns 404 rather than 400.
  • An item a caller can list but not get is returned projected onto apiVersion, kind, and the metadata fields name, labels, effectiveLabels, and deletionTimestamp — no spec, status, uid, revision, or discriminator. Operators always hold get, so their list responses are unchanged apart from effectiveLabels.
  • A write that loses a serialization race after five attempts returns 503 with a retryable message instead of committing on stale facts.

A restriction must name a subject that resolves today. A principal’s own Group ties are read through a live, active User resource of their name, and no login path writes one yet, so every principal currently has an empty tie set. A group-targeted binding therefore grants nothing — harmless — but a cap expressed as a group-targeted Deny is equally never collected, so it does not restrict anyone either. Until identity resolution lands, express a restriction against system:authenticated, org:<name>, or the principal itself.

A GroupMembership you write now is not inert in one respect: the grant gate resolves a name’s ties without requiring the User row, so the marker is weighed against whoever later tries to create or activate that name. That is deliberate — it is what stops a deleted-and-recreated identity from silently reclaiming its old groups.

Two audit record names changed: resource.operator_status_updated and resource.operator_finalizers_updated are now resource.user_status_updated and resource.user_finalizers_updated, because the caller need not be an operator. Two records are new: resource.access_denied and resource.grant_gate. Update any log-based alerting that matches the old names.

No migration and no backfill runs, and no configuration changes. auth.operator_users and auth.operator_idp_groups keep their meaning.

Action requiredAll backendsChange

Action required if you author org RoleBindings — subject bounded to its own Organization

. An org RoleBinding whose subject names a different organization — group:<other>/x, serviceaccount:<other>/x, org:<other> — is now refused at write time. Such a binding never granted anything: ADR-0001 §1’s recipient boundary already required the subject to belong to the binding’s own Organization, so the row read as a cross-org grant while being permanently dead. Only the generic resource API is affected, and only for callers who may write policy resources there.

Existing rows keep being readable and keep granting exactly what they granted before (nothing), but an update to such a row now fails. To find them:

SELECT parent.name AS organization, r.name, r.spec->>'subject' AS subject
FROM resource_store.resources r
JOIN resource_store.resources parent ON parent.uid = r.parent_uid
WHERE r.kind = 'RoleBinding'
  AND r.deletion_timestamp IS NULL
  AND r.spec->>'subject' ~ '^((group|serviceaccount):[a-z0-9-]+/[a-z0-9-]+|org:[a-z0-9-]+)$'
  AND CASE
        WHEN r.spec->>'subject' LIKE 'org:%'
          THEN split_part(r.spec->>'subject', ':', 2)
        ELSE split_part(split_part(r.spec->>'subject', ':', 2), '/', 1)
      END <> parent.name;

Delete what it returns, or re-point each subject at a Group in the binding’s own Organization. user: and system:authenticated subjects are unaffected — their affiliation is a live membership question, not a property of the identifier — as are controller: subjects.

The same field now also accepts the relative form group:<name>, expanded against the parent Organization before storage, so group:platform under acme stores group:acme/platform. Absolute subjects are unchanged, and PlatformRoleBinding still takes absolute subjects only.

ChangeAll backends

No action required — seeded baseline authorization policy

. Startup now creates five root policy resources described by ADR-0001: PlatformRole/system-admin with its system:operators binding, and the editable PlatformRole/org-admin, PlatformRole/resource-owner, and PlatformRoleBinding/resource-owner defaults. Seeding is idempotent and never overwrites the three editable rows, so an operator edit survives every restart and a deleted one is re-created on the next.

These are the whole of the shipped policy: with the choke point live (above), an install that authors no bindings of its own grants nothing to anyone but operators and the subjects a rise.dev/owner label names.

Two of the five are immutable through the API: the resource store refuses to update or delete PlatformRole/system-admin or its binding, because they are the inspectable record of operator authority rather than its source (the evaluator hardcodes that, so it survives a bad restore). If startup ever reports one of those two as diverging from its shipped definition, something wrote to resource_store.resources directly; the error names the row and the fix is to delete it and restart. No migration and no backfill runs.

Config changeAmazon ECSFeature

Config change — Terraform modules for Amazon ECS

. A new modules/rise-ecs provisions a working ECS install (VPC, cluster, Cloud Map, RDS, Secrets Manager, NLB, Traefik and the Rise service), and modules/rise-aws gains enable_ecs for the control-plane IAM it needs — the task execution role, the scoped ECS statements, and iam:PassRole limited to those two roles. Both the VPC and the cluster are optional, so it deploys into infrastructure you already run. Purely additive: nothing changes for an existing install that does not set enable_ecs. See Terraform.

Action requiredAmazon ECSChange

Action required if you pin the AWS provider below 6.0 — the Terraform modules

. modules/rise-aws moves its required_providers constraint from >= 4.0 to >= 6.0, and modules/rise-ecs ships with the same floor. Two separate reasons, worth keeping apart:

The old >= 4.0 was simply wrong — the module has used aws_vpc_security_group_ingress_rule in its RDS section since that section was written, and the resource does not exist before provider 5.0, so the constraint only ever worked because nothing pinned an older provider.

The floor is 6.0 rather than 5.0 because 6.x is the only major the modules are tested against: mise run terraform:check resolves the latest provider, and provider majors change resource schemas. Stating a floor we do not verify invites a confusing failure inside the module rather than a clear one at terraform init. If you pin 4.x or 5.x, move to 6.x.

ChangeAll backends

No action required — Terraform pin.

The repo’s toolchain pin moves from 1.9.8 to 1.15.9 (mise.toml). It affects contributors running mise run terraform:check, not installs: the modules still declare required_version >= 1.5.0, and all 18 module tests pass on the new version.

FeatureAll backends

No action required — generic resource labels

. Resources in the generic resource API carry metadata.labels alongside metadata.annotations. The migration adds a column with an empty default, so existing rows and clients are unaffected and no backfill runs. Label keys use the Kubernetes-shaped grammar that policy labelSelector keys already use; values are capped at 63 bytes. A key becomes access-relevant only once a policy binding selects on it, and writing such a key then passes the write-time grant gate.

Action requiredAll backendsChange

Action required if conflicts exist — identity resource activation

(#421). Rise now activates the eight reserved rise.dev/v1alpha1 identity resource kinds in the PostgreSQL resource store. Before upgrading, remove any legacy ResourceDefinitions that claim those reserved group/kind or collection identities, and migrate or remove any stored identity rows whose structural parents do not match the built-in hierarchy. Startup fails closed when such conflicts exist and reports the total plus a bounded sample; use the previous Rise version to remove the conflicting definitions and rows, then recreate custom resources under a non-reserved identity if needed. Installations with no reported conflicts require no action.

Worth knowing before you start: the reservation is wider than the eight identity kinds. The whole rise.dev API group is now closed to external ResourceDefinitions, as are the eight identity collection names in any group (collection names have always been globally unique). The activation runs in one transaction, so an upgrade rejected by the audit leaves the database exactly as it was — clean up the conflicts it names under the previous Rise version and retry.

Config changeAll backendsChange

Config change — admin and Operator roles by IdP group

(#429). auth.admin_idp_groups and auth.operator_idp_groups grant the admin and Operator roles to everyone in the listed IdP groups, so the IdP stays the source of truth instead of an email allowlist that has to be edited and redeployed. Both default to empty, so installs that grant roles by email alone are unaffected and pay no extra query. A user holds a role if their email is on the allowlist or they are in one of the groups; group names match case-insensitively.

All group matching — including the existing auth.platform_access.allowed_idp_groups — now resolves against the IdP-managed teams Rise syncs from the configured group claim (auth.idp_group_claim, default groups), rather than against every team the user belongs to. Action required only if you granted platform access through allowed_idp_groups naming a team that Rise did not create from the IdP (i.e. idp_managed = false); those users lose platform access until the group comes from the IdP. This closes a privilege-escalation path where a user could create a team named after an allowed group and grant themselves access. Group membership refreshes at login, so revoking a group in the IdP takes effect on the user’s next login (or the next Entra active sync).

Config changeAll backendsChange

Config change — custom IdP group claim

. auth.idp_group_claim selects the ID-token claim Rise uses for IdP-managed team synchronization. It defaults to groups, preserving existing behavior. AWS Cognito operators can set it to cognito:groups (or set OIDC_GROUP_CLAIM=cognito:groups in the standalone Docker deployment).

Action requiredAll backendsChange

Action required if conflicts exist — policy resource activation

(#430). Rise activates the four reserved rise.dev/v1alpha1 policy resource kinds — Role and RoleBinding under an Organization, PlatformRole and PlatformRoleBinding at the root — in the PostgreSQL resource store. The same fail-closed pattern as the identity activation above applies: before upgrading, remove any stored rows in the rise.dev group using those four Kind names, and any ResourceDefinition claiming one of the four collection names (roles, rolebindings, platformroles, platformrolebindings) in any group. Startup reports the total plus a bounded sample and leaves the database unchanged, so clean up under the previous Rise version and retry. Installations with no reported conflicts require no action.

These resources are what the choke point (above) evaluates: writing a RoleBinding now grants access. Bindings are validated at write time, so creating one requires its roleRef target, its scope target, and any literal subject it names to already exist — create the Role before the RoleBinding that references it.

ChangeDocker

Behavior change — workload identity on the Docker backend

(#382, closing #378). The Docker controller now delivers the same workload-identity material as Kubernetes — the bootstrap credential and one token file per [identity].audiences entry — to /var/run/secrets/rise/identity/ inside each app container (via the Docker archive API), and refreshes the token files before they expire. No new configuration; this closes a parity gap, so a Docker app that sets [identity].audiences now receives its tokens instead of nothing. Identity files are delivered when a container is created, and the controller also self-heals already-running containers that lack them on the next reconcile, so apps running before the upgrade pick up their identity material without a redeploy (mirroring the Kubernetes controller re-establishing it on each sync).

ChangeKubernetes

Behavior change — workload identity token refresh on Kubernetes

(#390). The Kubernetes controller now runs a leader-elected loop that re-mints each deployment’s pre-minted identity token files before they expire. The sync webhook records a per-deployment due time (~2/3 of deployment_controller.identity_token_ttl_seconds after each mint); the loop resyncs a RiseProject only when one of its deployments is due. Metacontroller does not resync a steady project on its own, so previously a long-lived pod’s identity file token could expire without being refreshed (the on-demand token-exchange endpoint was unaffected). No new configuration and no action required; the only operational change is a rise.dev/trigger annotation write per due deployment (so projects are touched only when a refresh is needed, and the work is naturally staggered) and one more background lease (rise-identity-refresh). Docker already refreshed via its own reconcile loop, so this closes the gap on Kubernetes. The per-project re-mint due time is tracked on the RiseProject CR’s status.identityRefreshDueAt (written by the sync webhook), so there is no deployments-table schema change.

Action requiredAll backendsDeprecation

Action required — raw external token deprecation signal

(#394). While auth.allow_raw_external_tokens is true, each accepted raw-token request now emits one metric-shaped tracing event (target=rise::deprecation, metric=raw_external_token) carrying the validated issuer/sub. Aggregate it in your log pipeline (count, group by issuer/sub) to find which CI workload identities still present raw external tokens: the default flips to false in 0.25.0, after which those callers must pre-exchange at POST /api/v1/auth/token. No config change; migrate CI before upgrading to 0.25.0.

Config changeAll backendsChange

Config change — auth token exchange (phase 1)

(#367). Adds the RFC 8693 exchange endpoint and a Rise Access token kind. Purely additive; existing token flows are unchanged, legacy in-handler verification remains the fallback. See ROADMAP.md § “Workstream 2 — Authentication & Token Exchange”.

Config changeDockerChange

Config change — Docker deployment backend

(#358). Selectable via deployment_controller.type = "docker". Single-host; Kubernetes remains the default, so existing installs are unaffected unless they opt in. A new deployment rolls over via Traefik health checks, with old and new overlapping in one load-balanced service (a rolling update, vs. Kubernetes’ atomic blue/green). Probing is opt-in: no health_check means ready-when-running; a set health_check is a 2xx–3xx check. Operator-relevant settings on the Docker controller (env-driven; the shipped standalone compose sets working defaults):

  • traefik_api_url (default in-network http://rise-traefik:8080) — the rolling gate reads Traefik’s serverStatus, the authoritative readiness signal for health-checked containers (no fallback). The standalone Traefik enables its API internally (--api.insecure=true, port not published). If you run your own Traefik and any project uses a health_check, you must expose its API to the backend over the internal network (optionally with basic-auth embedded in the URL); without it a health-checked deployment never becomes Healthy. It may be left unset only when no project uses health checks.
  • Replicas: the Docker config raises deployment_constraints.max_replicas to 10 (RISE_MAX_REPLICAS); the controller additionally hard-caps at 50. The deployment-backend feature matrix, the Docker operator pages, and the cutover/health-check docs ship with this PR.
Action requiredAll backendsChange

Action required — reserved RISE_ env-var prefix

(#355). User-supplied environment variable keys beginning with RISE_ are rejected at the API and at deploy time (project env vars and per-container [containers.X.env]). If any of your users’ apps set RISE_* keys, rename them before upgrading.

Config changeAll backendsChange

Config change — Operator role (auth.operator_users).

The generic resource API (/api/v1/resources) is gated to a new, separately configured Operator role. auth.admin_users do not receive Operator access. No action needed unless you want operators to manage generic resources. See ROADMAP.md.

Config changeAll backendsChange

Config change — default Organization / Kubernetes controller_class_name.

Backend startup bootstraps a single default Organization and backfills existing users, teams, and projects to it under an advisory lock. Existing installs resolve to the same namespace names as before (rise- prefix → rise-myapp). The Kubernetes controller’s controller_class_name defaults to a stable value for existing installs if unset.

Security and upgrade guidance

BreakingAll backendsFix

Forwarded client addresses are honoured only from trusted proxies

IP-keyed rate limits on unauthenticated endpoints (device-login start, token exchange, OAuth extension endpoints, workload-token rejections) no longer trust X-Real-IP / X-Forwarded-For from arbitrary callers. Rise reads them only when the TCP peer is inside the new server.trusted_proxies list, and resolves the client from X-Forwarded-For right to left, skipping trusted hops. X-Real-IP is used only when there is no X-Forwarded-For. IPv6 clients are rate-limited per /64. The default list is loopback, RFC 1918 and IPv6 unique-local space, which also contains co-tenant workloads: any app or pod that can reach Rise’s port from a private address can set X-Forwarded-For and choose its rate-limit bucket.

BreakingAll backendsFix

Credentials are bound to their resource and purpose

Startup automatically imports stored secrets into context-bound envelopes. OAuth secrets also bind the client and token destination; use canonical token URLs and reseal credentials when that configuration changes.

BreakingAll backendsFix

Sign-in and administrator defaults are stricter

Sign-in requires verified email and browser proof cookies; missing group claims refuse login by default. Rise pages cannot be embedded in iframes. There is no default administrator, and placeholder keys require explicit development opt-in.

Action requiredAll backendsFix

Project, registry, and custom-domain access is tightened

ECR credentials bind the repository, project UUID, and credential generation; orphaned images are not inherited. Custom domains cannot overlap control-plane hosts. Policy-audit, project ownership, team visibility, and deleting-parent checks enforce access.

FixAll backends

OAuth extension test requires a signed-in project member

“Test OAuth” in the Rise UI now requires a signed-in browser session of a user with platform access and access to the project, and the callback must complete in that same session. Only a test run this way updates the extension’s stored status; its failure text is limited to the upstream’s OAuth error and error_description. A failed end-user login no longer marks the extension configured or clears its error.

BreakingAll backendsFix

CLI secrets use safer files and logging

Pack uses private env files and rejects CR/LF, NUL, and trailing-whitespace values. Secret files are private, Compose files are backend-scoped, and logs omit credential values. Docker build-argument argv exposure remains deferred; avoid passing credentials through Docker build arguments.

BreakingKubernetes · Amazon ECSFix

Helm and ECS require explicit security configuration

ECS workloads require a dedicated task role. Helm supports OIDC secrets through env/envFrom, ships no demo Dex users, and narrows Metacontroller rules. Startup budgets accommodate credential migration.

Config changeAll backendsChange

Decoded token claims are logged only on the opt-in rise::auth::idtoken target

The backend no longer logs the IdP ID-token payload at debug, and the CLI no longer logs the decoded claims of the token it presents at debug. Both emit the decoded header and claims at trace on the rise::auth::idtoken target, which RUST_LOG=debug does not enable. The payload contains personal data such as email, name and groups; the signature is never logged.

Action requiredAmazon ECSFix

ECS Traefik receives the real client address

The rise-ecs Terraform module now configures Traefik to keep the client address the load balancer reports. Previously Traefik discarded it and recorded the load balancer node’s private address, so Rise saw every request as coming from inside the VPC.

  • alb-acm: Traefik’s web entrypoint trusts X-Forwarded-For and X-Forwarded-Proto from the public subnets; applications now see the ALB’s X-Forwarded-Proto (https for TLS-terminated requests).
  • nlb-traefik-acme: the NLB target groups enable PROXY protocol v2, and Traefik reads it on web, websecure and ping from the public subnets. ACME HTTP-01 and the ping health check keep working.

The client is the rightmost untrusted entry of X-Forwarded-For; the leftmost entries behind the ALB are client-supplied. Rise resolves it that way through its trusted-proxy list.

Ship this together with the trusted-proxy client-address resolution. Before it, Rise prefers X-Real-IP, which Traefik now passes through from the client on the ALB path, so this change alone opens a spoofing window.

ALB only, applications: trusting the ALB also leaves the client-supplied X-Forwarded-Host and X-Real-Ip intact (the ALB sets neither); Traefik previously overwrote both, as Kubernetes nginx and Docker still do. Applications that build URLs from X-Forwarded-Host become host-header-injectable on ECS behind an ALB. Use the Host header instead. Rise itself is unaffected. The NLB mode trusts no forwarded header and is unaffected.

Security groups are unchanged. The new client_ip input of the runtime submodule defaults to none, so direct deployments are unaffected.

Known release-candidate limitations

Known limitationDocker

Docker drift-driven recreation can briefly interrupt a single replica.

A normal deployment cutover uses the documented health-driven rolling overlap, but replacing an already-running container whose runtime configuration drifted still removes the old container before creating its replacement. Multi-replica workloads recreate one healthy replica at a time; a single-replica workload has a short gap. Tracked in #379.

Known limitationDocker

Docker HA status writes have a bounded stale-leader race.

Destructive Docker operations re-check leadership immediately before acting. The slower health reconciliation pass can, however, finish a status-only database write after its previously confirmed lease window has elapsed. A current leader corrects that status on a later tick; no stale leader performs a container create or removal through this path. Tracked in #401.

Known limitationDocker

This release has no earlier Docker-enabled upgrade baseline.

The upgrade harness already supports the Docker backend, but every release before this one predates the standalone Docker stack. CI therefore runs the in-place upgrade suite on Kubernetes for this release. Publishing this version establishes the Docker baseline; enable the Docker upgrade job for the next release cycle so subsequent versions exercise image and database migrations from it.

Rise 0.22.0

ReleasedGitHub release ↗

This release adds JFrog Artifactory and AWS S3 as first-class extensions, introduces per-deployment resource configuration, hardens authentication security, and overhauls the documentation site.

New Features

FeatureAll backends

JFrog Artifactory registry support

— mint scoped, short-lived credentials for image push and Kubernetes pull secrets via JFrog’s access token API or a Vault plugin. Separate push/pull TTLs, per-project scope isolation, and pull credential caching included.

FeatureAll backends

AWS S3 bucket extension

— automatically provisions a dedicated S3 bucket and scoped IAM credentials per project, injected as environment variables. Supports DeletionBlocked state when a bucket is non-empty, with opt-in force_empty_bucket for safe teardown.

FeatureAll backends

Configurable deployment replicas and resources

— set replicas, cpu, and memory in rise.toml or via --replicas/--cpu/--memory CLI flags. Admins can set per-environment min/max constraints; values are validated at deployment time.

FeatureAll backends

Starlight documentation sites

— user and engineering docs now live at docs/user and docs/engineering with improved structure and navigation.

FeatureAll backends

rise backend rise-toml-schema CLI command

— generates the rise-toml-v1.schema.json directly; the old /api/v1/schema/rise-toml/v1 endpoint now redirects (301) to /docs/schemas/rise-toml-v1.schema.json.

Bug Fixes

FixAll backends

Auth: cookies scoped to exact host

— cookies no longer carry a Domain attribute, preventing cross-subdomain leakage between the Rise API and app subdomains. Ingress auth flows now always redirect through /.rise/auth/complete on the app’s own domain.

FixAll backends

Auth: ingress JWTs now carry the correct aud claim

— private-app JWTs previously used the Rise server URL as audience; they now use the app’s own URL. API middleware rejects RS256 ingress tokens, preventing them from authenticating API calls.

FixAll backends

AWS error detection

— replaced brittle string-matching on formatted error types with typed AWS SDK error variants across S3 and RDS extension providers.

⚠️ Breaking Changes

BreakingAll backendsChange

cookie_domain setting repurposed

— this field no longer sets the Domain attribute on new cookies. It is now used only to clear stale domain-scoped cookies during migration. Existing configs continue to work, but cross-subdomain cookie sharing via cookie_domain is no longer supported.

BreakingKubernetesChange

Legacy Metacontroller adoption code removed

— the legacy_adopt_existing_resources_to_metacontroller backend setting and adoptExistingResources Helm value have been removed. Clusters that still rely on this one-time migration path must complete the migration before upgrading.

BreakingAll backendsDeprecation

Registry credentials endpoint moved

— credentials are now fetched from GET /projects/{name}/deployments/{id}/registry-credentials (scoped to Pending/Building/Pushing states) instead of the old project-scoped endpoint. Older CLI versions fall back to credentials in CreateDeploymentResponse, which is now deprecated.

Dependency Updates

MaintenanceAll backends

Dependency updates

Routine updates to tokio, openssl, aws-sdk-s3/s3-js, jsonwebtoken, tailwindcss 4.3, vite 8, react 19.2, pack 0.40.6, gunicorn 26, and several other crates and npm packages.

Rise 0.21.1

ReleasedGitHub release ↗

Overview: A single bug fix addressing network connectivity issues with ingress controllers.

Bug Fixes

FixKubernetes

Fix NetworkPolicy blocking ingress traffic

— Updated the NetworkPolicy to allow ingress controller traffic to reach the main HTTP port, resolving potential connectivity issues where applications were unreachable despite correct ingress configuration. (#262)

Rise 0.21.0

ReleasedGitHub release ↗

This release focuses on security hardening, high-availability support, and a major architectural change to the Kubernetes deployment controller.

New Features

FeatureKubernetes

Metacontroller-based deployment controller

— Migrated the Kubernetes deployment controller from an in-process controller to a Metacontroller webhook architecture (d65ecf7, c57ba8b)

FeatureAll backends

High-availability backend

— Backend is now safe to run with multiple replicas, including HA lease liveness and retryable OAuth state (7077511, fe18842)

FeatureKubernetes

Deployment secrets as Kubernetes Secrets

— Environment variable secrets are now injected as proper Kubernetes Secrets instead of inline values (bd10719)

FeatureKubernetes

Pod-IP validation for webhooks

— Replaced token-based webhook auth with pod-IP validation for improved security (a48c586)

FeatureAll backends

--platform build option

— Override the default build platform (e.g., --platform linux/amd64) (3626399)

FeatureAll backends

rise env export

— New command to export environment variables, with a fix to environment resolution in rise run (08b6b67)

Bug Fixes

FixAll backends

Fix handling of terminating pods in deployment status display (6c044ba)

FixAll backends

Reject plain PKCE method in OAuth flow, require S256 (4b42ae3)

FixKubernetes

Fix podSelector in webhook NetworkPolicy for metacontroller (6909f0f)

Other Changes

ChangeAll backends

Added cargo audit to CI (d3541cb)

ChangeAll backends

Dependency updates: openssl, pack (buildpacks), marked

ChangeAll backends

Development documentation and task improvements

ChangeAll backends

Updated tag-release script

Rise 0.20.2

ReleasedGitHub release ↗

Two bug fixes improving OAuth security controls and environment variable handling.

ChangeAll backends

OAuth rate limiting is now per-project, per-IP, and configurable

— Rate limiting for OAuth token exchanges is scoped more granularly instead of being global, and can now be tuned via configuration. (#247)

ChangeAll backends

Environment variable source display and scoped propagation

— Fixed how environment variable sources are displayed and how environment-scoped variables are propagated to deployments. (#246)

Rise 0.20.1

ReleasedGitHub release ↗

This release focuses on configuration improvements, OAuth fixes, and developer experience enhancements.

New features

FeatureAll backends

Public JSON Schema endpoint for rise.toml

— A new endpoint serves the JSON Schema for rise.toml, enabling editor autocompletion and validation (#243)

FeatureAll backends

rise.toml is now deploy-time config

— The configuration is read at deployment time rather than being baked into the project, making it easier to iterate on build/deploy settings (#244)

FeatureAll backends

Improved local dev setup

— Pod security disabled by default (needed for Railpack/root builds), and docker-compose.yaml now sets a name field for cleaner container naming

Fixes

FixAll backends

Allow PKCE and client_secret together

for confidential OAuth clients (#245)

FixAll backends

Enforce environment default/production flag invariants

— Prevents invalid states where multiple environments claim to be the default or production environment (#242)

FixAll backends

Use hostPort for ingress-nginx

in k3s dev setup, fixing local routing issues (#241)

FixAll backends

Set fallback value for environment_ingress_url_template

— Prevents missing config errors (#236)

Other

ChangeAll backends

Added .gitattributes for .sqlx/*.json files (linguist/diff handling)

ChangeAll backends

Updated Rust dependencies (patch-level updates for aws-sdk-kms, reqwest, and others)

ChangeAll backends

Updated frontend dependency jose to v6.2.3

ChangeAll backends

Documentation: enforced mandatory cargo fmt before every commit

Rise 0.20.0

ReleasedGitHub release ↗

This release introduces first-class environment support, source metadata tracking, and several security and reliability improvements.

ChangeAll backends

First-class environments support

— Environments are now a core concept, enabling separate deployment targets (e.g., staging, production) within a single project (#224)

ChangeKubernetes

Per-environment Kubernetes ServiceAccounts and labels

— Each environment can have its own ServiceAccount and Kubernetes labels for better isolation and RBAC (#227)

ChangeAll backends

Source metadata on projects and deployments

— Projects and deployments now track source information (e.g., git repository, commit) for traceability (#229)

ChangeAll backends

OAuth rate limiting

— Token, authorize, and callback endpoints are now rate-limited to prevent abuse (#230)

ChangeAll backends

Prevent team deletion when team owns projects

— Teams can no longer be deleted if they still own projects, avoiding orphaned resources (#228)

ChangeAll backends

Updated Rust dependencies (patch versions) (#217)

ChangeAll backends

Updated npm dependencies (patch versions) (#220)

ChangeAll backends

Updated Python dependencies: packaging to v26.2 (#226), click to v8.3.3 (#222)

ChangeAll backends

Reverted premature version bump to 0.19.4

Rise 0.19.0

ReleasedGitHub release ↗

This archive records the deployment-status API deprecation. See the GitHub release for the complete release notes.

Action requiredAll backendsDeprecation

Deployment status updates require a project-scoped endpoint

The project-scoped status endpoint identifies the deployment using both project name and deployment ID. The unscoped endpoint is deprecated and emits a warning when called. See #182.

Rise 0.15.7

ReleasedGitHub release ↗

This archive records the OAuth credential migration. See the GitHub release for the complete release notes.

BreakingAll backendsDeprecation

OAuth client_secret_ref requires migration to encrypted storage

In 0.15.7, the OAuth reconciler reads the referenced environment variable, encrypts its value into client_secret_encrypted, removes client_secret_ref from the stored spec, and attempts to delete the environment variable. Direct runtime lookup through client_secret_ref is removed. See #155.

Rise 0.13.1

ReleasedGitHub release ↗

This archive records the OAuth credential deprecation. See the GitHub release for the complete release notes.

Action requiredAll backendsDeprecation

OAuth client_secret_ref is deprecated

OAuth extensions support client_secret_encrypted for direct encrypted storage of the upstream provider’s client secret. The environment-variable reference client_secret_ref is deprecated. See #125.

Planned changes

Not yet merged

Not yet merged. Each item is tracked by a rollout-gate issue and moves to Unreleased when its implementation lands.

0.25.0

BreakingAll backendsDeprecation

Breaking — raw external tokens are refused by default.

auth.allow_raw_external_tokens defaults to false, so a service account must exchange its external OIDC token at /api/v1/auth/token before calling project-scoped endpoints. With that default, direct external-token presentation is rejected unless an operator explicitly enables the compatibility setting. No removal version or date is announced. Prepare now: aggregate the rise::deprecation raw_external_token events to find the workload identities that still present raw tokens, and move their CI to pre-exchange (see Authentication & Tokens). Tracked in #374.

Not yet scheduled

BreakingAll backendsChange

Breaking — organization linkage becomes mandatory.

The organization_resource_uid columns on teams and projects become NOT NULL once startup backfill is proven reliable. Backend startup already backfills them, so no action is expected; the release that tightens them will say if that changes. Tracked in #372.

Action requiredAll backendsChange

Action required (for controller authors) — organization opt-in for controllers.

Controllers that register ResourceDefinitions in an organization’s own API group will need an explicit, operator-granted enablement step per organization. The design is still open. Tracked in #437.

FeatureAll backends

Resource API — list pagination and label/field selectors.

list and list_versions return every row today; pagination and selectors land before high-cardinality kinds move onto the resource store. Additive. Tracked in #368.

FeatureAll backends

Resource API — watch / change feed.

Lets controllers reconcile without polling list. Additive. Tracked in #369.

FeatureAll backends

Resource API — secret fields.

Write-once, never-read-back values in the resource store. Additive. Tracked in #370.

ChangeDocker

Docker — no outage when a single-replica container is recreated.

The replacement container starts before the old one is removed, closing the short gap a drift-driven recreate leaves today. No action needed. Tracked in #379.

What do the impact labels mean?
Breaking
Requires action or changes behavior incompatibly. Read the instructions before upgrading.
Action required
The upgrade succeeds, but an operator action is needed for the expected behavior.
Config change
Configuration is available or changes; defaults preserve existing behavior.
No action required
Features, fixes, and maintenance with no required operator action.