Releases: l0rdg3x/OPNGMS
Release list
v0.22.5
[0.22.5] - 2026-07-30
Security
- The backend image no longer ships
pip. Trivy reported two HIGH findings against the image, and
both came from pip's own vendored dependencies —msgpack 1.1.2(GHSA-6v7p-g79w-8964, SEGV/DoS
when anUnpackeris reused after an error) andsetuptools 70.3.0(CVE-2025-47273, path traversal
inPackageIndex) — declared inpip/_vendor/vendor.txt. Neither is installed standalone, and
neither is reachable: the container only ever runsuvicorn,arqoralembic, never pip.
Upgrading pip cannot clear them either — no release, 26.2 ormain, vendors patched versions. So the
Dockerfile now uninstalls pip once the application is installed, which removes the vulnerable code
from the image and drops a package manager from a production container on general principle.
python -m ensurepiprestores pip inside a running container if it is ever wanted for debugging; the
bundled wheel stays and Trivy does not look inside it.- Verified by rebuilding the real image and rescanning it with the exact CI invocation
(trivy:0.65.0 image --severity HIGH,CRITICAL --ignore-unfixed): 2 findings → 0.app.main
still imports, anduvicorn,alembicandarqall still start.
- Verified by rebuilding the real image and rescanning it with the exact CI invocation
- Correction to the 0.22.4 notes, which claim "zero open alerts". That was verified true before the
release branch was pushed — and it stopped being true nine minutes before the merge, when the release
PR's own Trivy scan created the two alerts above. Two lessons, recorded rather than glossed: the
Trivy (backend)/Trivy (frontend)checks are green by design (--exit-code 0), so they are a
baseline that publishes findings to the Security tab, not a gate that blocks on them; and an alert
count read before a branch is pushed says nothing about the tree that gets tagged. Re-read it after the
final push, not before.
v0.22.4
[0.22.4] - 2026-07-29
Changed
- Dependency maintenance — ten merged pull requests, no behaviour change. Every one landed with the
full CI suite green:- CI: actions/setup-python 6 → 7, actions/setup-node 6 → 7, and docker/login-action
4.4.0 → 4.6.0 (theactions-minorgroup). Pull requests put both majors through their paces via
ci.yml, but they also sit outside it —setup-pythoninpublish-catalogs.ymland
scheduled-audit.yml,setup-nodeinscheduled-audit.yml— and no pull request runs those. The
refreshedlogin-actiondigest is used bytrivy.yml, already green onmain, and by
publish-images.yml, where this release is its first run. - Backend: the declared floors now match what CI had already been resolving — pydantic-settings
≥ 2.14.2, httpx ≥ 0.28.1, webauthn ≥ 3.0.0, and ruff ≥ 0.16.0 (dev). These four are
floors with no ceiling, so pip takes the newest release the rest of the graph allows on every CI
run; the bumps close the gap between the declaration and the versions the suite had been passing
against all along. (Where a ceiling does exist, the newest release is not what gets installed —
see the redis entry below.) - Frontend: the
frontend-minorgroup — Mantine 9.4.2 → 9.5.0 across all six packages,
recharts 3.10.0 → 3.10.1, eslint 10.7.0 → 10.8.0, globals 17.7.0 → 17.8.0 — plus the
test-only jsdom 29.1.1 → 30.0.1 and @testing-library/jest-dom 6.9.1 → 7.0.0.
- CI: actions/setup-python 6 → 7, actions/setup-node 6 → 7, and docker/login-action
Fixed
-
Two dependency ceilings are now declared rather than rediscovered. Both were proposed as updates,
both fail, and neither failure is caused by this codebase:- redis stays on 5.x.
arq— the task queue the worker runs on — pinsredis[hiredis]>=4.2.0,<6,
and 0.28.0 is its latest release, so raising the floor to>=8.0.1made the dependency set
unsolvable (ResolutionImpossible); the failed install took three CI jobs with it (backend tests,
dependency audit, and the backend Trivy scan, which builds the image).backend/pyproject.tomlnow
declaresredis>=5.0,<6with arq named as the source, so the limit is visible where the dependency
is written instead of being rediscovered from a red build. It duplicates arq's constraint, so the
comment says to drop it rather than raise it once arq widens — otherwise the line we added to
explain a blocked upgrade becomes the thing blocking one.- Worth stating plainly: this parks the queue's Redis client on an unmaintained line. redis-py
5.3.1 (2025-07-25) is the last 5.x release and the branch has had no commits since. Nothing
affects it today — pip-audit is clean and no advisory covers 5.x — but a future redis-py CVE would
have no in-place fix; the way out would be arq widening its range, or being replaced. That is the
accepted risk, not an absence of one.
- Worth stating plainly: this parks the queue's Redis client on an unmaintained line. redis-py
- TypeScript stays below 6.1, which the existing
~6.0.2pin already enforces. On TypeScript
7.0.2tsc -b,vite buildand all 298 frontend tests pass —npm run lintis what aborts: no
publishedtypescript-eslintdeclares support for TypeScript ≥ 6.1 (8.65.0 and even its canaries
declare>=4.8.4 <6.1.0; upstream tracking issue typescript-eslint/typescript-eslint#10940). No
Dependabot ignore rule was added for it — a silenced update is worse than a visible one — so the
next proposal gets re-evaluated rather than skipped.
Both are recorded in
AGENTS.mdso the next contributor reads the reason before spending a CI run
on it. - redis stays on 5.x.
-
A contribution rule the repository states but does not enforce.
AGENTS.md, the Wiki and the
header comment inci.ymlall asserted that a pull request must be up to date withmainbefore it
can merge — andci.ymlused that assertion to justify being pull-request-only, on the grounds that
the PR run therefore validates the exact tree that lands. The ruleset has
strict_required_status_checks_policyoff: no rebase is required, so apull_requestrun — which
tests the head merged into the base tip at the time it runs — leaves a check that can be stale by
the time the merge happens. Eight of this release's ten updates merged while behindmain(#247
went first, #254 was rebased by Dependabot). All three texts now describe that, and prescribe the
remedy: after merging a batch, re-run the suite locally on mergedmainbefore tagging.- The same look at the ruleset turned up a second gap worth writing down. On a Dependabot pull request
the three requiredAnalyze (…)contexts never run — CodeQL default setup skips them — so
required checks are never satisfied and the merges go through the admin bypass
(RepositoryRoleadmin,bypass_mode: always). Turning the strict policy on would therefore not,
by itself, have blocked these merges. Both are repository settings rather than code, and both are
left as open decisions.
- The same look at the ruleset turned up a second gap worth writing down. On a Dependabot pull request
-
The weekly dependency audit was running on Node 20.
scheduled-audit.yml— the only job that
re-checksmainbetween pull requests — set up Node 20 while CI uses 24. This release made that worse
rather than better: jsdom 30 and@testing-library/jest-dom7 both declareengines.node >= 22, as
react-router 8 already did, sonpm ciin the weekly baseline was resolving the lockfile under an
engine none of them support. Now pinned to 24, matching CI and the documented Node 24+ requirement. -
The CodeQL query suite was named wrong in 0.22.3. That release's notes and the Wiki's security page
both saidsecurity-and-quality. GitHub's default setup does not offer that suite — it offers
default or extended, and this repository is on extended, which resolves to the
security-extendedbuilt-in suite (confirmed in the config API and in every analysis's SARIF
codeqlConfigSummary). The difference is not cosmetic:security-and-qualitywould additionally run
the whole quality query set — maintainability and reliability, in GitHub's own wording — which is not
running here. The Wiki is corrected; the 0.22.3 entry is left as written, with the correction recorded
here instead of edited into history.
Security
- No new advisories, and the one carried-over residual is now actually patched. CodeQL default
setup's extended suite (security-extended, over Python, JavaScript/TypeScript and Actions)
reports zero open alerts — though "open" is the operative word: seven alert records remain on file
as dismissed, and four of those findings are still produced by the current scan ofmain. One is
py/request-without-cert-validation, dismissed as won't fix — the accepted-risk decision recorded
in 0.22.3.npm audit --omit=devandpip-auditare both clean. - brace-expansion 2.1.2 → 2.1.3 in the lockfile. 0.22.3 recorded this nested copy — reached by
openapi-typescript→@redocly/openapi-core1.x →minimatch5.x — as having no fix on the 2.x
line. That is no longer true: upstream backported the guard (EXPANSION_MAX_LENGTH) to 2.1.3 on
2026-07-28, and it satisfies minimatch's declared^2.0.1, so a lockfile refresh takes it without
touching the codegen tool. The unbounded-expansion crash (CVE-2026-14257) is therefore gone from the
tree. A fullnpm auditstill flags it, because the advisory's vulnerable range is the flat
<= 5.0.7and has not been narrowed the way its sibling GHSA-3jxr-9vmj-r5cp was — so the noise
outlives the defect. Everything about the placement is unchanged: dev-scope, offlinenpm run gen:api
only,npm ciinstalls it in the frontend build stage but the shipped nginx image copiesdist/
alone, and the production-only audit gate never saw it.
v0.22.3
[0.22.3] - 2026-07-25
Security
- Four open dependency advisories closed — one of which had already broken the CI security gate.
A fullnpm audit(rather than only the single alert Dependabot had raised) surfaced four HIGH
findings in the frontend:- react-router → 8.3.0 (GHSA-qwww-vcr4-c8h2), a production dependency. The CSRF bypass lives
only in React Router's unstable RSC code paths, which this SPA does not use — but 8.3.0 is the
only patched version, and the advisory was already failingnpm audit --omit=dev, the command
behind the "Dependency audit" CI job. Becauseci.ymlis pull-request-only,mainnever went red;
the next PR would have. In v8 thereact-router-dompackage no longer exists, so the 22 files that
imported from it now import fromreact-router— that is the entire change.useMatches, the one
breaking change v8 brings to a declarative SPA, is not used here. - brace-expansion → 5.0.8 (GHSA-3jxr-9vmj-r5cp exponential-time expansion, and the
unbounded-expansion OOM advisory) and js-yaml → 4.3.0 (GHSA-52cp-r559-cp3m, quadratic CPU on
merge-key chains). Development-scope, lockfile only. A second, nested copy of brace-expansion
reached its 2.x maintenance ceiling at 2.1.2: that closes the expansion DoS, but the OOM
advisory has no fix on the 2.x line, so it stays flagged in a fullnpm audit. It sits under the
offlinegen:apicodegen tool, never ships in an image, and does not affect the production-only
audit gate.
- react-router → 8.3.0 (GHSA-qwww-vcr4-c8h2), a production dependency. The CSRF bypass lives
- The device form now surfaces the TLS trust decision instead of burying it. Triaging a CodeQL
finding on the connector (py/request-without-cert-validation) confirmed the flagged behaviour is
deliberate —Verify TLSis a per-device operator setting, on by default, that exists because
OPNsense appliances ship self-signed certificates — but it also exposed a real gap: the API accepted
a pinnedtls_fingerprintas the compensating control and no part of the UI ever offered it.
An operator who switched verification off got no warning and no way to pin. Switching it off now
reveals a warning stating plainly that nothing authenticates the device's certificate and that
credentials can be intercepted, next to the fingerprint field that restores protection. The
fingerprint is sent only when verification is actually off and a value was entered, so neither an
empty field nor a dormant leftover is ever stored as if pinning were active. Translated across all
13 UI languages.- Still open, and stated honestly: an already-onboarded device cannot have its TLS settings
changed from the console — the API supports it, no UI exposes it. Re-add the device to change its
pinning.
- Still open, and stated honestly: an already-onboarded device cannot have its TLS settings
Changed
- Routine dependency maintenance (no behaviour change; all CI-verified, and both major bumps are
covered by tests that exercise the real API rather than a mock — the report renderer builds an actual
PDF through WeasyPrint'sURLFetcher, and the Fernet/x509 paths run end to end):- Backend: cryptography 42 → 49 and WeasyPrint 62 → 69 (majors), Alembic 1.13 → 1.18.5,
FastAPI 0.138 → 0.139, aiosmtplib 5.1.1 → 5.1.2. - CI: the grouped
actions-minorbump — dockerbuild-push-action7.2 → 7.3,metadata-action
6.1 → 6.2,setup-buildx-action4.1 → 4.2,setup-qemu-action4.1 → 4.2, and alogin-action
digest refresh. - Frontend: the grouped
frontend-minorbump (18 updates — Mantine 9.4.0 → 9.4.2, React 19.2.6 →
19.2.8, Recharts 3.9 → 3.10, Vite 8.1.0 → 8.1.5, ESLint 10.5 → 10.7, plus msw, TanStack Query and
typescript-eslint).
- Backend: cryptography 42 → 49 and WeasyPrint 62 → 69 (majors), Alembic 1.13 → 1.18.5,
Fixed
- Documentation drift in the Wiki. Its security page asserted that the repository has no CodeQL
workflow. CodeQL is in fact active through GitHub's default setup — thesecurity-and-quality
suite over Python, JavaScript/TypeScript and Actions — which is precisely why nocodeql.ymlexists
to be found. The scan table now lists it, and the Transport-security section documents outbound TLS
to managed firewalls, which it had never covered.
v0.22.2
[0.22.2] - 2026-06-24
Changed
- Routine dependency maintenance (no behaviour change; all CI-verified, and the two major bumps
API-checked against our usage). No new CVE fixes this round — the security scans (CodeQL, gitleaks,
Trivy, and thepip-audit+npm auditdependency audit) are clean.- Backend: FastAPI → 0.138 (minor; the httpx / starlette upper-bound caps are still satisfied),
maxminddb 2 → 3.1.1 (major — the production GeoIP reader pathReader(BytesIO, mode=MODE_FD)+
.get()was verified against 3.1.1, on top of the existing real-mmdb reader tests), and dev tooling
respx → 0.23.1, pyotp → 2.10, ruff → 0.15.19. - CI: actions/checkout 6 → 7 (the v7 fork-PR hardening only affects
pull_request_target/
workflow_run, neither of which this repo's workflows use). - Frontend: the grouped
frontend-minorbump (12 minor/patch updates — Mantine 9.3.2 → 9.4.0,
Recharts 3.8 → 3.9, Vite 8.0 → 8.1, plus TanStack Query, typescript-eslint, and friends).
- Backend: FastAPI → 0.138 (minor; the httpx / starlette upper-bound caps are still satisfied),
v0.22.1
[0.22.1] - 2026-06-19
Security
- Dependency security updates. Two dependencies bumped for published CVE fixes:
- Jinja2 → 3.1.6 (GHSA-cpwx-vrp4-4pq7 — the
|attrfilter no longer bypasses the sandbox's
attribute-lookup checks). - undici → 7.28.0 (frontend dev/transitive) — closes 7 advisories, including two High-severity
ones: a WebSocket DoS via unbounded message fragments (CVE-2026-12151) and a SOCKS5 ProxyAgent TLS
certificate-validation bypass (CVE-2026-9697).
- Jinja2 → 3.1.6 (GHSA-cpwx-vrp4-4pq7 — the
Changed
- Routine dependency bumps (no behaviour change; all CI-verified, and the major bumps API-checked
against our usage):- Backend: aiosmtplib 4 → 5.1.1 (API-compatible; XOAUTH2 is now first-class upstream), Babel →
2.18, and dev tooling pytest → 9.1 / ruff → 0.15.17. - CI image build: docker/build-push-action 6 → 7, metadata-action 5 → 6, setup-buildx-action
3 → 4, setup-qemu-action 3 → 4 (Node 24 action runtimes; the inputs we pass are unchanged). - Frontend: the grouped
frontend-minorbump (11 minor/patch updates).
- Backend: aiosmtplib 4 → 5.1.1 (API-compatible; XOAUTH2 is now first-class upstream), Babel →
v0.22.0
[0.22.0] - 2026-06-17
Added
- SMTP OAuth "Connect" button (experimental). The OAuth2 SMTP relay (Gmail / Microsoft 365) gains a
"Connect with Google / Microsoft 365" button under Admin → SMTP delivery that runs the
authorization-code flow and stores the obtained refresh token automatically — instead of pasting it by
hand. The operator still registers the OAuth app and enters the client id + secret; the button only
removes the manual refresh-token paste. The browser flow uses a signed, expiringstate(HMAC over
the initiating superadmin + provider, 10-min TTL) as its CSRF defence, and the redirect URI + landing
URL are built only from the newPUBLIC_BASE_URLsetting (https-only; no open-redirect). The
refresh token is Fernet-encrypted at rest like every other secret; the flow is superadmin-gated and
audited (smtp.oauth.connected).⚠️ Experimental — untested against a live provider. The browser consent + real Google/Microsoft
redirect could not be verified in CI (no public callback URL / registered OAuth app); the code is
unit-tested with mocked HTTP. The UI labels the button "Experimental — untested", and pasting a
refresh token manually remains the supported path. The Connect button requiresPUBLIC_BASE_URL
set and the redirect URI registered in your OAuth app.
v0.21.0
[0.21.0] - 2026-06-17
Added
- Remember-this-device (trusted devices). After clearing a second factor (TOTP or WebAuthn), users
can tick "Trust this device for N days" at the login MFA step; subsequent logins from that device
still require the password but skip the second factor until the trust expires. Trust is
server-side and revocable: a newtrusted_devicetable stores only anHMAC-SHA256(SESSION_SECRET)
hash of a token that lives in anHttpOnly+Secure+SameSite=Laxcookie (a DB dump yields no usable
tokens; rotatingSESSION_SECRETinvalidates every trusted device).- Manage your trusted devices under Two-factor auth — list (device, IP, last used, expires),
revoke one, or revoke all (with confirmation). - Auto-revoke: disabling your own MFA, an admin MFA reset, or log out everywhere drops all of a
user's trusted devices (a plain logout intentionally keeps the trust so the feature survives it). - Org controls (superadmin): a Remember-this-device on/off toggle (env default
TRUSTED_DEVICE_ENABLED=true) and a runtime-tunable trusted device lifetime (trusted_device_days,
default 30, range 1–365) under System → Runtime settings. - Safety rails: the password is always required; trust is bound to one user (a cookie for user A can
never skip MFA for user B); a trusted cookie can never bypass mandatory MFA enrollment; expired or
revoked cookies fail closed; a sweeper purges expired rows. Every action is audited
(auth.login.trusted_device,auth.trusted_device.create/revoke/revoke_all/policy_change).
- Manage your trusted devices under Two-factor auth — list (device, IP, last used, expires),
v0.20.0
[0.20.0] - 2026-06-16
Added
- WebAuthn / passkeys as a second login factor. Alongside TOTP, users can now register passkeys —
platform authenticators (Face/Touch ID, Windows Hello) or roaming security keys (YubiKey) — and
satisfy the login MFA challenge with a passkey or a TOTP code. Register one or more passkeys under
Two-factor auth (an account-password step-up is required, like TOTP enrolment); each is named and
individually removable, and the last remaining factor can't be removed while the MFA policy requires it.
Verification follows the WebAuthn ceremonies (challenge + RP ID + origin checked server-side; sign
count must strictly increase to detect a cloned authenticator); the per-ceremony challenge is
server-generated, single-use, and bound to the session. WebAuthn is public-key — only the public key- sign count are stored (no secret material). Because passkeys bind to a domain, set the relying-party
config first (System → WebAuthn:rp_id/origin, also viaWEBAUTHN_RP_ID/WEBAUTHN_ORIGIN);
registration is disabled until it is configured. Backend + frontend (#207, #208), adversarial
security-review + CodeQL clean. (remember-this-deviceis a planned follow-up.)
- sign count are stored (no secret material). Because passkeys bind to a domain, set the relying-party
v0.19.0
[0.19.0] - 2026-06-16
Added
- OAuth2 (XOAUTH2) authentication for the SMTP relay — Gmail + Microsoft 365. Google Workspace and
Microsoft 365 are disabling basic-auth SMTP, so the report-email relay can now authenticate with
OAuth2 instead of a password. In System → SMTP, pick OAuth2, choose the provider (Google /
Microsoft 365), and provide theclient_id/client_secret/refresh_token(plus the Azuretenant
for Microsoft); OPNGMS exchanges the refresh token for a short-lived access token at send time and
authenticates with SASL XOAUTH2 (via aiosmtplib's native support). Password auth is unchanged. The
OAuthclient_secret+refresh_tokenare Fernet-encrypted (MASTER_KEY) like every other secret —
never returned by the API (onlyhas_…flags) and never logged; the access token lives only in memory
for the send. The Send-test button exercises the OAuth path too, and the MASTER_KEY rekey tooling covers
the new encrypted columns. This is the manual-entry core; an in-app "Connect" button (OAuth
authorization-code callback) is a planned follow-up for deployments with a public callback URL. Hardened
against the CodeQL extended suite (the Microsoft tenant is sink-guarded against partial-SSRF). (#205)
v0.18.1
[0.18.1] - 2026-06-16
Fixed
- Business→Community map dropped Business hotfix releases. v0.18.0's
business-base.jsongenerator
only matched a "based on the OPNsense X.Y.Z community version" header, but a Business hotfix
(e.g.25.4.3,24.4.3) is instead "based on the OPNsense X.Y.Z business version" — it chains onto
the prior Business release. Those entries silently fell out of the map. The generator now captures the
community/businesskind and follows a business→business chain transitively to the underlying
Community base (25.4.3 → 25.4.2 → 25.1.12), skipping only genuinely unresolvable chains or cycles. It
also no longer floors the map at major ≥ 25 (the Community-catalog floor is enforced downstream by the
resolver), so the published map is complete — every documented Business release (38 entries from the
currentopnsense/changelog) resolves to its Community base instead of just the 8 direct-Community ones.