v2.152
What's Changed
Security and RFC-compliance hardening pass across all PKI protocols and resource APIs.
Smoke-tested across SQLite and PostgreSQL on Debian, RHEL/Fedora, and Docker (33 migrations from-scratch on PG verified).
Security
- Certificate authorities — whitelist key params, cap validity at 3650 days, lock HSM-bound key, validate URLs (CRL DP / AIA / OCSP / IDP), cap bulk and list operations, harden create/update/export. CSR signing now verifies
is_signature_valid(proof of possession). EC curve restricted to a whitelist. - Certificates — whitelist key params, cap validity, fix unhold bugs.
- CSRs — cap validity, validate keys, verify CSR pubkey ↔ submitted match, cap PEM size 64 KB.
- Templates — cap validity_days, whitelist key_type/digest, fix import NULL.
- Policies / approvals — enforce group gate, expiry and validity reclamp.
- Users — require current password for self-change, protect last admin (≥1 active admin invariant).
- RBAC — validate role payload, reject reserved names (
admin/operator/viewer), permission whitelist with wildcard. - SSO — add PKCE (S256) and nonce to OIDC auth flow.
- HSM — encrypt provider secrets at rest, cap sign payload at 1 MiB, FK-guard deletes; runtime
pip installdisabled by default (opt-in viaUCM_ALLOW_RUNTIME_PIP=1). - Microsoft CA — fail-closed encryption, EOBO admin gate, audit, size caps.
- Webhooks — encrypt secret at rest, validate event names against allowlist, lock reserved headers, cap events per webhook.
- Discovery — validate ports, IPv6 subnet cap (≤1024), gate
update_profile. - Audit — trusted-proxy XFF (only honour
X-Forwarded-Forfrom configured proxies), fix invalid kwargs in ACME audit, post-cleanup integrity check. - Reports — cap generate params dict size (DoS guard).
- SSH — validate sign/generate payload (caps on principals ≤64, extensions/options ≤32, validity 60 s – 10 y).
- Trust store — whitelist purpose, cap PEM size 256 KB, sync limit 1–1000.
- ACME (server) — close 6 RFC 8555 auth bypasses (account binding, order ownership, authz state machine, finalize URL, key change, deactivation).
- ACME (proxy) — block SSRF via forged proxy IDs; finalize ownership check.
- ACME (client) — validate domain syntax, cap inputs, harden commits.
- EAB — encrypt HMAC keys at rest.
- EST — proof of possession, serialize bug fixes, config bound validation.
- SCEP — tighten challenge auth, audit reads, validate config bounds.
Fixed (RFC compliance)
- OCSP (RFC 6960) — handle mixed-format serials in DB lookup (decimal / lower hex / upper hex), invalidate cache on revoke, correct
keyHashcalculation, honournonceextension (skip cache when present), refuse delegated responder cert withoutid-pkix-ocsp-nocheck. - CRL (RFC 5280) — handle mixed-format serials, drop silent truncation of serials >159 bits, auto-regen expired CRL on CDP fetch.
- Certificate profile (RFC 5280) — 5 issues fixed in CA/CSR signing paths (SKI/AKI format, BasicConstraints encoding, EKU consistency, KU bit ordering, validity bounds).
- ACME (RFC 8555 / 8737) — EAB JWK match via thumbprint, JWS algorithm allowlist (asymmetric only), wildcard domain restricted to DNS-01, ALPN extension marked critical, case-insensitive domain handling. Pre-authorisation flow (§7.4.1) —
acme_authorizations.order_idnow nullable (migration 033). - TSA (RFC 3161 / 5035) —
signing-certificate-v2ESS attribute now mandatory, request body capped at 64 KiB, correctPKIStatusseparation fromPKIFailureInfo. - EST (RFC 7030) —
serverkeygenencrypts the server-generated key under the client mTLS pubkey, not under the newly issued cert. - SCEP (RFC 8894) — reject renewal when signer cert is expired or not yet valid.
Fixed (other)
- Imports — CA and certificate import endpoints now encrypt private keys via
encrypt_private_keyinstead of storing base64-plain (silent regression introduced when import paths bypassed the lifecycle mixin).
Added
tools/decode-csrandtools/decode-cert— input capped at 256 KiB →413(DoS guard).
Tests
- 1676 backend tests + 461 frontend tests pass.
- 2 encryption-related tests made hermetic to host
master.key(monkeypatchMASTER_KEY_PATHand useis_string_encrypted()).
📜 Recent release history (last 2 versions)
[2.151] - 2026-05-07
Fixed
- ACME proxy did not enforce External Account Binding (#112) — when
acme_eab_requiredwas enabled, the proxy directory at/acme/proxy/directoryadvertised upstream Let's Encrypt'smetaas-is (which does not require EAB), so clients like win-acme reported "server does not indicate that this is required" and proceeded to register an account without anexternalAccountBinding. The proxyPOST /acme/proxy/new-accountlikewise never inspected the payload for an EAB field and accepted any registration. The proxy now (1) overridesmeta.externalAccountRequiredwith the local UCM policy in its directory, and (2) validates theexternalAccountBindingJWS in/new-accountexactly like the local server, rejecting registrations without a valid HMAC binding when EAB is required. - Local
/acme/new-accountreturned 500 on empty body —request.get_json()raised on requests withContent-Type: application/jose+jsonand an empty body instead of producing a clean400 malformed. Now usesforce=True, silent=Trueso empty/invalid payloads return the documented ACME error. - ACME admin UI 404 on account detail (#113) — the admin routes
GET/DELETE /api/v2/acme/accounts/<id>,POST .../deactivate,GET .../orders,GET .../challengesonly resolved by numeric primary key, but the UI passes the publicaccount_idstring. Newresolve_acme_account()helper accepts either form and is used by all five admin routes; protocol routes (RFC 8555) are unchanged. refactor(acme): bind directory_url to account key via AcmeClientAccount table— landed in this release: the on-disk account key file is now selected by joiningAcmeAccount.directory_urlto the matchingAcmeClientAccountrow, removing the previous heuristic that looked up the key by directory URL string match alone.
Tests
tests/conftest.py::create_user— made the factory idempotent on session-DB collisions, so a test that re-uses a user hint (or a sharded CI re-run) no longer fails with HTTP 500 on the second create. The full backend suite (1676 + 209 ACME + frontend 461) is green on all three distros.
[2.150] - 2026-05-07
Fixed
- ACME default environment ignored when payload omits it (#26) —
POST /api/v2/acme/client/accountsandPOST /api/v2/acme/client/ordersalways defaulted to staging (environment='staging') when the request body did not specify one, even though Settings → ACME → Let's Encrypt let operators pin a default environment viaacme.client.environment. New ACME accounts and on-demand orders created from the frontend (which never sendsenvironmentin the body) silently went to staging instead of production. Both endpoints now readSystemConfig['acme.client.environment']when the field is omitted, falling back to'staging'only if no default is configured. The frontendACMEPagemodal now also waits forclientSettingsto load before opening, eliminating a race that briefly defaulted the dropdown to staging at mount. - DELETE on templates and certificates failed on FK violation —
DELETE /api/v2/templates/<id>did not check if the template was referenced by certificates or policies before issuing the delete, raisingIntegrityError(HTTP 500) when the template was in use. Now blocks with409 Conflictand a "used by N certificate(s) / N policy/policies" message; operators must remove the dependents first.delete_certificateinservices/cert/mixins/lifecycle.pydid not clean upApprovalRequestrows pointing to the certificate, hitting the same FK class of failure on certs that had ever been the subject of an approval workflow. Cleanup is now wrapped in a try/except with explicit rollback. - Unprotected
db.session.commit()in 39 service-layer call sites — bare commits across 20 service modules (acme_renewal, audit/query, auto_renewal, backup/restore_core, ca/{ca_creation,ca_crud,ca_operations,ca_signing}, cert/mixins/{csr,import_export,lifecycle}, crl/generation, discovery/{profiles,query,scanner}, hsm/hsm_service, opnsense/config, policy_service, ski_aki_backfill, template_service) had no surroundingtry/except. On any commit failure (constraint violation, disconnected DB, deadlock) SQLAlchemy left the session in a broken-transaction state, causing every subsequent request handled by the same worker to fail withPendingRollbackErroruntil the worker recycled. All 39 sites are now wrapped:try: db.session.commit(); except Exception as e: db.session.rollback(); logger.error(..., exc_info=True); raise— same caller-side behavior, just a guaranteed rollback. - PostgreSQL upgrades crashed at boot on migration 030 (#111) —
030_add_certificate_san_upn._upgrade_pg(engine)openedwith engine.begin() as conn:but the migration runner already passes the live, transactionalConnectiontomod.upgrade(conn)(fixed in #103/#104). Calling.begin()on an already-boundConnectionraisessqlalchemy.exc.InvalidRequestError: a transaction is already begun on this connection, killing the boot of every PostgreSQL install upgrading to v2.149. Reported by @Hemsby. Migration 030 now uses the runner-suppliedConnectiondirectly. The same latent bug existed in eight earlierpg_compatiblemigrations (020, 021, 022, 023, 024, 025, 026, 027 EAB, 027 backfill SAN email) — they were invisible because previously applied installs never re-ran them, but a fresh PostgreSQL install would have hit the same crash on first migration. All nine are converted to the_upgrade_pg(conn)shape and use the supplied connection directly. - Test guard against future regressions — added
test_pg_migrations_do_not_open_nested_transactionsintests/test_migration_runner_pg.pythat AST-scans everypg_compatiblemigration and rejects any call toengine.begin()/conn.begin(). The pre-commit hook will catch any new migration that re-introduces the pattern. End-to-end checktest_pg_migration_runs_against_real_postgreswas rerun against a real PostgreSQL 15 container and passes.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.152
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.152
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.152Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.152/ucm_2.152_all.deb
sudo dpkg -i ucm_2.152_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.152/ucm-2.152-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.152-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.152_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!