You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
[1.1.2] - 2026-10-16
Security
While a person's temporary password must still change (a new account an
admin created, or a temporary password an admin set), their session can
only read their own profile, change the password and use MFA. Every other
API call from that session, creating an API token included, answers 403
with password_change_required, their live streams are refused, and an
open dashboard tab goes to the password change page. Before, only the web
app asked for the change, and the API served that session in full,
including creating API tokens.
Listing the Rosters a person leaves on demotion or deactivation now checks
that the person belongs to your workspace, like the other People routes
(404 otherwise). Before, it answered for a person of another workspace;
one instance runs one workspace, so this only mattered if a second
workspace row existed.
Model discovery (GET /models) now accepts connection settings (a base
URL, a key variable, an API version, a region, a profile, a project or a
location) only from admins; every other role, and their API tokens, gets 403 before anything is read or called. Before, any signed-in user, a
Viewer included, could name an environment variable and a URL, and
OpsMender sent that variable's value to the URL as a bearer key, which
could expose the token signing secret, the database URL or a provider key.
A model's key setting must now be written like an environment variable
name (letters, digits and underscores, not starting with a digit);
discovery and the model save routes refuse anything else with 400
before reading or storing it, without repeating it. Settings saved before
this change are not rewritten. An unknown provider in discovery answers 400 instead of a server error.
The generic command guard now also catches common runner names: terminal, console, tty, pty, shellexec, bash_tool, invoke_expression, subprocess, popen, os_system, sudo, su, docker, podman, nerdctl, crictl and systemctl, names that start with bash_, powershell_, pwsh_, sh_, terminal_, console_, sudo_, docker_
or subprocess_, and names that end with _terminal, _console, _bash, _powershell, _pwsh, _subprocess or _cypher, in any case or separator
style (Terminal, BashTool, bash-tool, PowerShellExecute). It also
catches the runner tools of common MCP servers, such as kubectl_generic, call_kubectl, call_aws, execute_kubectl, ssh_execute, start_process, execute_code, browser_run_code_unsafe and explain_sql_statement (which runs the statement when it analyzes it); the
MCP Skills page lists all of them and the bounded tools to classify in a
Skill instead. Before, terminal, shellexec, bash_tool and the like were ordinary tools, so a Skill entry
or glob that allowed one let it run at Tier 0, without approval at Tier 1,
and in environment scans. Now they are blocked at Tier 0 and Tier 2 and in
environment scans. At Tier 1 they ask for operator approval, and run once
approved, even where the Skill blocks them: the guard is checked before a
tool's own tier policy, so a T1 policy of blocked or advisory, or no
entry for the tool, no longer refuses it. To block one at every tier, add it
to the Skill's deny-list with deny: true. A narrowly scoped tool that a
new prefix or suffix catches, such as docker_logs, console_url, get_console or aws_console, needs an entry with its exact name and allow_generic: true to run under its own tier policies again. Skill
Studio's Generate from tools and AI assist now mark these names as generic
and pre-set them to deny, as they already did for the older runner names.
OIDC sign-in and Teams actions now name the signature algorithms they
accept. An identity provider's id token must be signed with an asymmetric
algorithm that its discovery document lists (RS256 when it lists none), and
a Teams action with RS256, the algorithm Bot Framework signs with. Before,
both checks used whatever algorithm the token named (python-jose advisory GHSA-3qf3-8w2g-rqmx), and a token naming HS256 or none caused a server
error instead of a refusal. An identity provider set to sign id tokens with
HS256 (the client secret), or with an RSASSA-PSS algorithm (PS256 and up)
that the token library cannot verify, is now refused with a clear message;
set it to RS256 or ES256. Both checks also try only the published keys
that can verify the token (only those with its key id when any key has
it), so a key set that lists an EC key before the RSA key, or holds a key
that cannot be used, no longer refuses a valid token.
The web app is now built with Next.js 16.4.1 (from 16.2.3) and tested with
Vitest 4.1.11 (from 3.2.4), which carry the fixes for the published
advisories against both, including three Next.js advisories from
2026-10-14 that 16.4.0 does not fix. None reached a running OpsMender: it
serves a static export with no Next.js server or image optimizer. The
locked Mako moves to 1.4.3.
Every pull request and release now audits dependencies: the web app's
runtime npm packages fail the check on a high or critical advisory, and
the installed Python packages fail it on any known advisory (checked
against OSV). Three advisories are ignored because they do not apply: GHSA-3qf3-8w2g-rqmx in python-jose (every token check names its
algorithms), GHSA-wj6h-64fc-37mp in ecdsa (it affects signing, which
OpsMender never does with ecdsa) and GHSA-84m7-p3x7-pcfv in the MCP SDK
(it affects MCP servers; OpsMender only connects to them as a client).
Fixed
Assigning an incident to someone else, and bulk reassign or bulk
acknowledge for someone else, now refuse the change when the incident's
owner changed after the request was checked, naming the current owner if
there is one (409, or that incident's failure in a bulk action); nothing
is written. Before, an assignment that arrived a moment after someone took
the incident replaced them without a note or an Inbox notice. An
assignment that arrives as the incident is resolved or merged is refused
the same way; before, it answered a server error or reported success.
Alert intake refuses a body whose objects and arrays nest more than 64
levels deep with 400 and opens no incident; the token's delivery log keeps
a short row naming the limit, never the body. Every other route refuses a
JSON body nested more than 64 levels with 400 before the route acts on
it, and the Teams, Discord, Slack and ticket webhooks, which read their own
bodies, refuse one too deep to read with their usual 400. A token nested
too deeply to read (a session, an MFA challenge, an SSO or bot sign-in
state, a voice link or a Teams token) is refused like any bad token.
Before, intake accepted and stored deeply nested bodies and failed with a
server error on the deepest, and a deeply nested body or token sent to
other routes, such as saving My Routing settings, failed with a server
error, sometimes after the value was saved.
On SQLite, the standalone binary's local database, identical alerts that
arrive at the same moment now open one incident and page once: the app takes
incoming alerts one at a time inside its process. Before, each alert could
open its own incident and page again. Run one app process per SQLite
database; several processes need PostgreSQL.
Inbox quiet hours and My Routing quiet hours are now saved separately, and
saving one page never changes the other. Before, both pages wrote one
shared setting: saving the Inbox notification preferences, even just a
category mute, turned off the quiet hours that hold back P1 pages, saving
My Routing turned off Inbox quiet hours, and after any Inbox save, Save
routing failed with "[object Object]". A save the server refuses as
invalid now shows the reason, in My Routing and the other pages. Through
the API, Inbox quiet hours are under routing.in_app.quiet_hours of /users/me/notification-preferences, and its quiet_hours is My
Routing's only. Upgrading moves saved Inbox quiet hours to their new
place; SQLite evaluation databases are not migrated, so set Inbox quiet
hours again there.
Reloading the Reliability page, or following a link out of the app, while
it is still loading no longer logs "Failed to load reliability data" in the
browser console: the browser cancels the page's requests then, and a
cancelled or aborted request is not a failure. A load that really fails is
still logged, also after you move to another page of the app.
Saving My Routing through the API with only one of the Inbox settings
under routing.in_app (a category mute, say) no longer drops the other
(the Inbox quiet hours); the settings are merged with what is saved. The
My Routing page never sends them, so this only affected API calls.
Changed
The People guide now says what a Viewer sees: in the web app the Dashboard,
the incident list and each incident's summary, the On Call Schedule,
Activity, their Inbox and their own profile; through the API also the lists
of Escalation Chains, Rosters, teams, services, memories and more, but not
incident timelines or comments, reports or analytics. A Viewer can't list or
open AI sessions or read their chat history, but sees each session's tool
calls in Activity and its proposed actions in the approval list, can read
which memories a session used, and can follow a running session's live
stream, chat included, if they know its id. What a Viewer can open is
unchanged; tests now hold the sidebar, the page guard, these API answers and
the session content a Viewer reads to it. Before, the Auth guide said
Viewers could read sessions and the People guide didn't say what a Viewer
could open.
The two quiet-hours settings now say which is which. The Inbox preferences
page (Inbox, then Preferences; it was called Notification preferences) calls
its setting Inbox quiet hours, says it only affects Inbox notices, and links
admins and operators to My Routing. The quiet hours in My Routing say they
hold pages and link back to the Inbox preferences page. Before, both pages
just said "quiet hours", so setting one looked like setting the other.