v0.4.0
Added
- Full user management from the UI. Settings → Users now edits a user's
name and role grants, resets passwords (with every session of the affected
user signed out), and deletes users — an explicit second step available
only after disabling, amending the original disable-only decision
(design/2026-08-06-users-crud-password.md). A new Settings → Account tab
lets any signed-in local user change their own password (current password
required; other sessions are evicted, the active one stays). Password
operations are refused for SSO users — their credential lives at the
identity provider.
Security
- An admin could mint a working local password for an SSO-only account.
PUT /api/v1/users/{id}accepted apasswordfor any user regardless of
origin, and neither the email lookup nor the password check filtered on it —
so the new credential was a genuine, working login that bypassed the
identity provider along with its MFA and conditional-access policy. Password
edits are now allow-listed onorigin=local(a future origin defaults to
refused), on both the admin route and the new self-service one. Deleting an
SSO user is also now spelled out in the UI as what it is: it removes only
the local record, and becausedisabledis the flag the SSO callback
checks, deleting a disabled SSO user undoes their lockout. - Rotating IP addresses bypassed the login lockout entirely. Both rate-limit
axes keyed on the client IP (email|ipandip), so an attacker spreading
guesses across N addresses got N × 5 attempts per minute against a single
account and tripped neither — a botnet, or any cloud NAT pool, made the
per-account lockout decorative. A third axis now counts failures against the
account alone (20 per minute), for password login and self-service password
change alike. It is a deliberate trade: sustained failures against one address
will keep that account's login blocked, bounded to a self-healing one-minute
window, never affecting established sessions or successful logins. - An SSO login could take over a local account's email and break its login.
auth_userhas no unique index and the SSO callback upserts by subject
without consulting the address, so an IdP user whose email matched a local
account added a second row sharing it — and the password-login lookup, which
had noORDER BY, then resolved to an arbitrary one of the two. Anyone able
to set their own email claim could aim that at the bootstrap admin. The lookup
is now local-first, and password login is allow-listed onorigin=local
rather than relying on SSO rows happening to carry an empty password hash.
Fixed
-
An install whose schema migration never ran now repairs itself instead of
failing every query forever. Schema is applied by amigrateJob that Helm
runs as apost-install/post-upgradehook — and Helm runs those hooks only
after--waitsucceeds. A release that timed out waiting for any component
(a slow image pull across a DaemonSet is enough) never created the Job, while
the Deployments Helm had already applied rolled out normally. The result was a
cluster that looked healthy and answeredUnknown table expression identifier 'auth_user'to everything, with four subsystems retrying forever at WARN and
none of them naming the problem. The hub now checks its schema on connect and
applies the missing migrations itself (hub.autoMigrate, on by default; the
embedded migrations are idempotent and safe to run concurrently with the Job
or another replica). When it can't — no DDL rights, or self-heal switched off
— it logs one ERROR naming the remedy rather than a warning flood, and
Settings → Status gains a Schema component showing applied/expected. The
migrate Job also stops deleting itself on success, sokubectlcan answer
"did the migration ever run?", anddeploy/install.shwaits 10m rather than
6m before giving up. -
Setting a ClickHouse database other than
otelno longer silently breaks
the install.clickhouse.external.databaseis a documented, schema-checked
value, but every migration file hardcoded anotel.prefix: the DDL landed in
otelwhile the hub queried the configured database and found it empty —
producing exactly the missing-table failure above, permanently. The migrations
now name their database through a placeholder the migrator substitutes, and
the configured name is validated as an identifier at boot. Installs on the
defaultotelare byte-for-byte unaffected. -
A node without RAPL no longer takes the whole sensor down. Enabling green
collection on a fleet of VMs put the sensor DaemonSet into CrashLoopBackOff:
the pinned Kepler exits at startup when it finds no powercap zones (failed to initialize service rapl: no RAPL zones found) instead of idling, and a
container that terminates itself keeps the pod out of Ready no matter how
few probes it carries — sohelm --waitandkubectl rollout statusfailed,
and logs, traces and metrics went down with an optional energy signal. The
measured source can now be dropped on its own with
sensor.green.kepler.enabled=false, leavingsensor.green.estimationto
feed/green(a guard refuses to leave both sources off, which would ship an
empty scrape config). Installs on RAPL hardware are unaffected — the flag
defaults totrueand those renders are byte-identical. -
Logging in through a reverse proxy that rewrites
Hostno longer 403s.
The hub's CSRF check compared the browser'sOriginagainst theHostit
received, so any proxy handing the cluster its ingress address instead of the
public domain turned every write — the login POST first of all — into
cross-origin request rejected. Two new chart values fix it without touching
the default, which stays strict:auth.trustedOriginsnames the origins that
are legitimate despite not matchingHost(the check stays on for everything
else), andauth.originCheck(enforce|log|off) lowers it when the
origins can't be enumerated —logallows the write and records the
Origin/Hostpair, which is how you find out what a proxy actually sends.
An install that sets neither renders the same manifest and behaves exactly as
before. Whenauth.oidc.publicUrlis set it is trusted automatically. -
A password change that half-applied reported itself as "internal error".
If the session sweep or the re-mint failed after the new password was already
saved, both the self-service and admin routes answered a generic 500 — which
reads as nothing changed, sending the user back to a password that no longer
works. Both seams now name themselves: the response states that the password
did change and what to do next (sign in again with the new one, or that other
sessions are still live and should be ended from another device). -
A default
helm installnow pulls the images it is supposed to. The
chart's image defaults never matched what the release workflow publishes, in
two independent ways: the repositories readavuruobs/hub(Docker Hub) while
releases pushghcr.io/avuruvision/avuru-obs-hub, and the tag defaults to
.Chart.AppVersion— bare SemVer, no leadingv— while onlyvX.Y.Zand
vX.Ywere ever pushed. Both are fixed: the four first-party repositories
now point at GHCR, and the release workflow additionally publishes the bare
X.Y.Ztag. Installs that already pass--set …repository/…tag(CI e2e,
private-registry overlays viaimage.registry) are unaffected. -
Green TDP estimation had no image at all.
sensor.green.estimation.image
shipped with an empty repository and no tag default, so enabling the feature
rendered an unusable image reference. It now defaults to the published
avuru-obs-tdp-estimatorat the chart's app version, andmake version-set
stamps that image alongside the other three.