Breeze RMM v0.102.0 — enrollment-link lifetime fixes, reliability root-causes, and security hardening.
Summary
- Enrollment links & installer tokens — expiry you pick is the expiry you get: independent bootstrap-token TTLs, a
maxEnrollmentLinkTtlMinutesceiling enforced on every mint path, and partner/org-level enrollment defaults with a settings UI (#2816–#2819). - Tenant offboarding drain state —
self_uninstallcommands are now deliverable to a churning tenant's agents instead of failing silently (#2781). - Reliability — four production issues root-caused, including metric-rollup retention that had never actually run and a deadlock that discarded agent inventory reports (#2786).
- Security — dependency CVE fix,
/partners/meresponse narrowed to an explicit column allowlist, CI workflow-trust hardening (#2783, #2779, #2800). - Internal improvements to hosted-platform operations tooling (#2778, #2780, #2782, #2795, #2799).
Added
- Partner/org enrollment defaults + settings UI — default link expiry, default device count, and a
maxEnrollmentLinkTtlMinutesceiling, settable at partner or org scope (stored in existing settings JSONB — no migration) (#2818, #2819) - Offboarding drain state — a terminal-intent
offboardingstatus for partners/orgs: users are locked out immediately while the agent channel stays open in a narrowed drain mode so queuedself_uninstallcommands actually deliver; a reaper flips the tenant tochurnedwhen the fleet drains or the window closes (default 72h, tunable viaOFFBOARDING_DRAIN_WINDOW_HOURS) (#2781)
Improved
- Add Device CLI tab — now has an expiry control, and the onboarding-token endpoint validates its request body (#2816)
- Internal operations and observability improvements for the hosted service (#2778, #2780, #2782, #2795, #2799)
Fixed
- Installer bootstrap tokens get an independent TTL — picking "30 days" in the Add Device modal no longer produces an installer that dies in ~60 minutes because the token inherited the transient parent key's lifetime (#2817)
- Four production root-causes (#2786):
- Metric-rollup partition maintenance ran owner-only DDL as the unprivileged app role and failed nightly — retention had never actually run; DDL now lives in
SECURITY DEFINERfunctions with pinnedsearch_path - Lock-order inversion between inventory ingest and risk-score refresh on
device_vulnerabilities— each deadlock discarded a full agent inventory report; both sides now lock in ascending id order with transient-error retry - Held-connection warnings carried an empty
workertag (~12k untriageable events) — BullMQ job execution is now wrapped in a tagged Sentry scope across all 38 workers - Device activity feed took ~26s because RLS made the
details->>'deviceId'index unusable — query restructured
- Metric-rollup partition maintenance ran owner-only DDL as the unprivileged app role and failed nightly — retention had never actually run; DDL now lives in
Security
- brace-expansion bumped for CVE-2026-14257 (#2783)
/partners/mereturns an explicit column list instead of the whole partners row (#2779)- CI workflow hardening — all workflow actions SHA-pinned,
persist-credentials: falseon checkouts, developer signing isolated (#2800)
Self-hosters are encouraged to upgrade.
Self-Hosting / Upgrade Notes
No breaking changes.
- Upgrade: bump
BREEZE_VERSIONto0.102.0in.env, thendocker compose pull api web portal && docker compose up -d(includeportal— it's a separate container). If building from source, runpnpm installfirst. - Database: 4 idempotent migrations auto-apply on API boot via
autoMigrate(unlessAUTO_MIGRATE=false). No large-table rewrites or backfills — two small internal tables, nullable columns onpartners, two enum values, and the metric-rollup maintenance functions. Verified against a non-superuser role (DO-manageddoadmin-style deployments are fine). - No new required environment variables. New optional ones (all have sane code defaults):
OFFBOARDING_DRAIN_WINDOW_HOURS(72),CHILD_ENROLLMENT_KEY_TTL_MINUTES(1440),INSTALLER_BOOTSTRAP_TOKEN_TTL_MINUTES(1440),INSTALLER_PARENT_MIN_REMAINING_SECONDS(60),ENROLLMENT_KEY_CLEANUP_ENABLED(true),ENROLLMENT_KEY_PURGE_AFTER_DAYS(7). - Behavior changes:
- Installer bootstrap tokens no longer expire with their parent enrollment key — long-lived installers now work as picked in the UI.
- Expired enrollment keys are swept and purged after 7 days by default (
ENROLLMENT_KEY_CLEANUP_ENABLED/ENROLLMENT_KEY_PURGE_AFTER_DAYSto tune). - The
offboardingtenant status exists but nothing enters it automatically — it's operator-initiated.
Full Changelog: v0.101.0...v0.102.0
What's Changed
- docs: v0.101.0 sweep + document PARTNER_API_CURSOR_SIGNING_KEY by @ToddHebebrand in #2770
- feat(api): offboarding drain state — deliverable self_uninstall for churning tenants (#2774) by @ToddHebebrand in #2781
- feat(api): acknowledgement routes for partner abuse signals by @ToddHebebrand in #2780
- feat(api): script-content detector for abuse-signals sweep by @ToddHebebrand in #2782
- fix(api): root-cause four production Sentry issues — BREEZE-10, BREEZE-3/W, BREEZE-9, BREEZE-B by @ToddHebebrand in #2786
- fix(api): return explicit column list from /partners/me instead of whole partners row by @ToddHebebrand in #2779
- feat(api): device IP scatter heuristic for abuse-signals sweep by @ToddHebebrand in #2778
- fix(api): label agent-WS DB contexts so BREEZE-A is triageable by @ToddHebebrand in #2795
- feat(api): billing identity abuse signals and suspended-partner subscription invariant by @ToddHebebrand in #2799
- fix(deps): bump brace-expansion to 5.0.8 for CVE-2026-14257 by @bdunncompany in #2783
- fix(web,api): Add Device CLI tab has no expiry control; validate onboarding-token body by @ToddHebebrand in #2816
- fix(api): installer bootstrap tokens get an independent TTL, uncapped by the transient parent key by @ToddHebebrand in #2817
- feat(api): enforce maxEnrollmentLinkTtlMinutes across every enrollment mint path (1/2) by @ToddHebebrand in #2818
- feat(web,api): partner/org enrollment defaults + settings UI (2/2) by @ToddHebebrand in #2819
- fix(ci): harden workflow trust and isolate developer signing by @ToddHebebrand in #2800
Full Changelog: v0.101.0...v0.102.0