v2.144
What's Changed
Added
utils/key_codec.py—load_pem_bytes(prv, *, context)/store_pem_bytes(pem)helpers that consolidate the previously duplicatedbase64.b64decode(decrypt_private_key(model.prv))pattern across 26 sites inapi/v2/*andservices/*. Errors now surface a caller-supplied context ("CA 42","certificate 17") instead of an opaquebinascii.Errorwhen a stored.prvis malformed or was encrypted with a differentKEY_ENCRYPTION_KEY.utils/db_transaction.commit_or_rollback()— boolean-returning service-layer counterpart tosafe_commit()(which is Flask-response-returning). Replaces 10 baredb.session.commit()calls inauth/unified.py,services/mtls_auth_service.py,services/webauthn_service.pythat previously could leak partial transactions on integrity errors.security/encryption.encrypt_text()/decrypt_text()— text-oriented helpers (PEM, JSON blobs, plain strings) that share the same wire format asencrypt_string()but never confuse the caller about the input contract. The mixedencrypt()(expects base64) vsencrypt_string()(expects text) split caused #105.- Generic release tooling —
scripts/smoke_release.py(auth/CDP/OCSP/EST/health probe),scripts/release_publish.sh(tag + GitHub release publish),scripts/wiki_release_notes.py(changelog → wiki page generator). Lab-specific hostnames removed; everything is parameterised viaUCM_BASEenv var. - CI workflows —
.github/workflows/tests.ymlruns the backend suite against both SQLite and PostgreSQL on every push (closes the gap that let #103 ship)..github/workflows/release-smoke.ymlrunssmoke_release.pyagainst the published artefacts after everyv*tag. - Pytest
postgresmarker — opt-in marker for tests that require a live PostgreSQL backend; skipped by default locally, always run in CI.
Fixed
- Silent
except Exception: passblocks in critical auth/security paths now log withexc_info=True. Specifically:auth/unified.py(4 sites: lockout config read, account-locked notification, login/logout WebSocket broadcasts, SMTP probe),api/v2/auth.py(7 sites: password policy import, password reset audit, etc.),security/csrf.py(CSRF token extraction failures),security/encryption.py,config/https_manager.py,services/audit/core.py,services/email_service.py,services/syslog_service.py,utils/backup_codes.py. These were not bugs in themselves but made post-mortem debugging of auth failures effectively impossible. - Latent #105-class regressions — 4 additional sites that round-tripped PEM through
encrypt()/decrypt()were migrated toencrypt_text()/decrypt_text(). - 10 bare
db.session.commit()sites in auth/mTLS/WebAuthn paths now wrap incommit_or_rollback()and rollback cleanly onIntegrityError.
Changed
- 26-site refactor to
utils/key_codec.load_pem_bytes(). Behaviour-preserving (asserted byTestEquivalenceWithLegacyPatterntest class); reduces import footprint (singleutils.key_codecimport vsbase64+security.encryption).
Tests
tests/test_key_codec.py(8 tests) — round-trip with/withoutKEY_ENCRYPTION_KEY, error messages with caller context, byte-for-byte equivalence with the legacy inline pattern.tests/test_db_transaction.py(5 tests) —commit_or_rollback()returns False + rolls back onIntegrityError, returns True on success, no double-rollback when called twice.tests/test_pem_encryption_helpers.py,tests/test_acme_proxy_key_encrypted.py,tests/test_key_encryption_pem_passthrough.py,tests/test_migration_runner_pg.py— regression coverage for #103/#104/#105 to prevent re-introduction.
Internal
- Backend suite: 1645 passed / 1 skipped (was 1632 / 1 in v2.143).
- Frontend suite: 461 passed.
📜 Recent release history (last 2 versions)
[2.143] - 2026-05-03
Fixed
- PostgreSQL migration runner crashed on startup when applying any pending migration written for the legacy
Engineinterface._run_pending_pg()now opens a single transactionalConnectionviaengine.begin()and passes it tomod.upgrade(conn), matching the SQLite path and the migration module signatures (#103, #104). Without this fix, fresh PostgreSQL deployments couldn't boot past first start, and existing PG instances couldn't apply any future migration. - ACME proxy account private key was stored in plaintext in
system_config. It is now encrypted at rest with the application key viaencrypt_private_key()/decrypt_private_key(), and existing plaintext keys are migrated transparently on first read (#105). KeyEncryption.decrypt()no longer raisesbinascii.Erroron PEM-formatted input that was never encrypted in the first place. The probe now isolates base64 detection from Fernet decryption so legacy plaintext keys round-trip cleanly through the new ACME proxy decrypt path.
Changed
- Cross-target release validation now covers PostgreSQL in addition to SQLite for every supported package (DEB, RPM, Docker). The PostgreSQL backend is now part of the mandatory pre-release smoke matrix because the #103 regression only manifested on PostgreSQL and would have shipped silently against a SQLite-only matrix.
[2.142] - 2026-05-02
Security
- EST
/cacerts,/simpleenroll,/simplereenroll,/serverkeygen,/csrattrsnow enforceest_enabledon every request and short-circuit with503 EST disabledinstead of falling through to the SPA. The/serverkeygenbody is also size-capped and stricter about content negotiation. - EST and SCEP mTLS client certificates are only honoured when the request comes from a trusted reverse proxy (
security.trusted_proxies). Direct hits without TLS termination by UCM no longer accept proxy-injected client cert headers. - mTLS login route gated behind trusted-proxy check. Same protection as EST/SCEP — header-based mTLS is rejected unless the request originates from a trusted proxy.
- 2FA backup codes are now hashed at rest (Argon2id) and consumed atomically; plaintext codes are returned only at generation time and never stored.
- Approval quorum is race-safe and idempotent. Concurrent approvals on the same request can no longer over-approve; double-submits are deduplicated.
- On-demand CRL generation is serialised per-CA with a per-CA lock and
503 Retry-After: 5under contention — closes a CPU/IO DoS vector when many clients hit/cdp/<ca>.crlsimultaneously. - Outbound webhooks revalidate the resolved IP at delivery time (DNS-rebinding window closed) and reject cloud-metadata IPs (
169.254.169.254, GCP/Azure/Alibaba equivalents) and loopback. RFC1918 /.lan/.localtargets remain allowed by design (UCM is on-prem). - SSO/IdP, ACME proxy and webhook URL fields all share the same SSRF helper (
validate_url_not_cloud_metadata) — cloud metadata is blocked everywhere. - CSV bulk user-import capped at 5 MB / 10 000 rows with
413on overflow. - Runtime HSM
pip installdisabled by default, returns403with a hint to setUCM_ALLOW_RUNTIME_PIP=1or install the dependency via the system package manager. - SCEP CSRs no longer copy arbitrary KU/EKU bits — only a whitelist (
digitalSignature,keyEncipherment,serverAuth,clientAuth) is honoured. - SCEP RFC 8894 P0/P1/P2 hardening — stricter PKCS#7 parsing, transaction-ID validation, signed/encrypted response envelope checks; iOS/macOS enrollment fixes (#102).
- ACME account private keys encrypted at rest with the application key.
- Password change endpoint ignores client-supplied
force_change(only operators can clear that flag). - CSRF token entropy increased; password hash algorithm tightened; database migration identifiers validated against an allow-list.
ProxyFixis opt-in viasecurity.trusted_proxies— prevents unauthenticatedX-Forwarded-Forspoofing on direct deployments.- Filesystem session directory is now created/enforced at mode
0o700and the application refuses to boot if it has group/world-readable bits. - EST audit lines use the trusted-proxy-aware client IP instead of the raw socket address.
Added
utils/trusted_proxy.py— sharedis_request_from_trusted_proxy()/client_ip()/reject_untrusted_proxy_headers()helpers used by EST, SCEP, mTLS login and audit.utils/ssrf_protection.py— single source of truth forvalidate_url_not_cloud_metadataandvalidate_host_not_cloud_metadata, used by webhooks, SSO, ACME proxy, OPNsense import.utils/safe_commit.py,utils/require_json_body,utils/parse_request_pagination,utils/safe_call,utils/audit_event— small composable helpers applied acrossapi/v2/*to remove boilerplate and silence intermittent rollback bugs.useCRUDPagefrontend hook covering 4 list/create/edit pages.
Changed
- Massive backend modularisation.
system.py(1556 l),certificates.py(2220 l),cas.py(1245 l),ssh_cas.py(1607 l),database_admin(817 l),discovery_service,pdf_generator,scep_service(981 l),acme_service(1456 l → 7 mixins ≤350 l),trust_store(1487 l),ca_service(788 l),restore_mixin,notification_service,audit_service,crl_service,ssh_cert_service,msca_service,account.py,acme.py,tools.py,acme_client.py,users.py,settings.py,opnsense_importandmodels/__init__.pywere split into focused submodules. Behaviour is unchanged; module size, test isolation and review surface improve. - Frontend modularisation.
CAsPage,CertificatesPage,DiscoveryPage,ACMEPage,SettingsPageandSsoProviderFormsplit into per-section sub-components underpages/<feature>/. - All
api/v2/*db.session.commit()calls now go throughsafe_commit()— consistent rollback + error logging on every write path.
Fixed
- PKCS12/PFX export now honours the
include_chainflag (#100). Previously the CA chain was always included, regardless of the request. - Dashboard chart cards no longer overflow the grid and System Health gained an internal scrollbar (#99).
- iOS/macOS SCEP enrollment regressions (#102).
Internal
- ~20 test files de-duplicated against
conftest.py; pre-commit i18n + 461 frontend + 1613 backend tests gate every commit. - RC validated end-to-end on Debian (
pve:8445), Fedora (fedor:8443) and Docker (pve:8444): smoke API 8/8 and Playwright use-cases 10/10 on all three targets.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.144
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.144
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.144Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.144/ucm_2.144_all.deb
sudo dpkg -i ucm_2.144_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.144/ucm-2.144-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.144-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.144_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!