What's Changed
Added
- Configurable public vhost for ACME directory URLs — a dedicated public hostname/port (
acme_public_vhost,acme_public_portin Settings → General) can now be advertised in the local ACME server and ACME proxy directory URLs (/acme/*,/acme/proxy/*), independent of the admin UI URL — the split admin/ACME reverse-proxy topology (e.g.admin.ucm.example.comvsacme.ucm.example.combehind one wildcard certificate). JWS verification accepts both the advertised public origin and the inbound URL; without a configured vhost, behavior is unchanged (request host). Wildcard hostnames are rejected for the vhost value (TLS SAN concept, not an advertised URL), and an optionalacme_public_tls_cert_idrecords which managed certificate to deploy on the ACME vhost (metadata only). Settings APIs exposeacme_public_base_url/acme_proxy_public_base_urland the UI directory URLs follow the configured origin (#173, thanks @fredlubrano).
Changed
- SQLAlchemy 2.0
Session.get— all 353Model.query.get()/query.get_or_404()call sites migrated todb.session.get()/db.get_or_404(), eliminating ~4500LegacyAPIWarningper test run ahead of a future SQLAlchemy major bump.
Security
- CSR extension policy on certificate issuance — the shared CSR signer copied every requested extension into the issued certificate verbatim, so a crafted CSR carrying
BasicConstraints CA:true(andkeyCertSign) submitted through EST, SCEP or ACME enrollment could obtain a working subordinate CA — a trust escalation from an enrollment endpoint. Leaf certificate types now forceBasicConstraints(ca=False)and strip the CA-only key-usage bits; only the explicit intermediate-CA signing flow may assert CA powers. Regression tested. - SCEP unauthenticated auto-enrollment when no challenge is set — per RFC 8894 §2.4 an omitted
challengePasswordallows unauthenticated authorisation, so with auto-approve enabled and no challenge configured any anonymous client on the public SCEP endpoint received a CA-signed certificate. InitialPKCSReqauto-issuance is now refused unless a challenge is configured (manual-approval mode and renewals are unaffected);UCM_SCEP_ALLOW_NO_CHALLENGE=1opts back in for isolated deployments. - Key-strength floor on EST and SCEP enrollment — both protocols now reject CSRs whose public key is below policy (RSA < 2048, non-NIST EC curves) instead of signing weak/exotic keys, matching the UI/API issuance floor (EST RFC 7030 §3.7 and SCEP defer key policy to the local CA).
- EST request-body cap bypass via chunked encoding — the body-size limit only inspected
Content-Length, so aTransfer-Encoding: chunkedrequest could stream an unbounded body into memory. The body is now read with a hard cap on the request stream regardless of framing.
Fixed
- Broken settings restore endpoint —
POST /api/v2/settings/backup/restorepassed a temp-file path where the service expects raw bytes, so it returned 500 on every call and left the uploaded (encrypted) backup in/tmpon each attempt. It now reads the upload as size-capped bytes in memory (no temp file) and restores correctly. - mTLS certificate import always failed —
POST /api/v2/mtls/enroll-importcrashed on every valid PEM (undefined variable, then a str stored into the binarycert_pemcolumn). The endpoint now imports and enrolls correctly; covered by a regression test. - Public ACME vhost hardening (post-#173 review) — the local ACME server now accepts the JWS
urlon both the advertised public origin and the inbound request origin (the tolerance moved intoverify_jwsitself, shared with the proxy, so in-flight orders survive anacme_public_vhostchange on/acme/*too); CAA enforcement andcaaIdentitiesfollow the configured public hostname instead of the inboundHost; the vhost is validated as a real FQDN (rejects.., leading/trailing hyphens, single labels); non-string vhost values and out-of-band garbage port rows return 400/defaults instead of 500; the public origin is memoized per request (one combined SystemConfig read instead of up to 10 per proxy request); the TLS certificate is picked from a dropdown of key-bearing certificates instead of a raw database id, and clearing it removes the config row; the CA Accounts panel shows the same public directory URLs as the other tabs.
📜 Recent release history (last 2 versions)
[2.186] - 2026-07-06
Fixed
- ACME proxy
/directoryon fresh install — the legacy/acme/proxy/directoryand/new-nonceendpoints returned a 500 (No external ACME CA account configured for the proxy) on a brand-new instance before any external CA account was added, because the proxy service resolved the upstreamAcmeClientAccounteagerly in its constructor. Resolution is now lazy: only the upstream directory URL (config-derived, defaults to Let's Encrypt staging) is needed for/directoryand/new-nonce, and the account row is looked up on the first key-bearing operation (new-account,new-order, signing), preserving the helpful configuration error for actual signing paths. This had broken the release smoke gate on a fresh Docker container. - ACME manual verify DNS wait and authorization poll cadence — the synchronous manual-verify endpoint ran the DNS self-check up to the full configured
dns_propagation_timeout(max 3600 s), risking client/proxy timeouts; it is now capped at 30 s (background auto-poll still honors the full timeout). The auto-poll loop also queried per-domain authorization status on every iteration, doubling CA traffic; it is now checked everymax(poll_interval × 5, 15)s, with the order-status poll still catching terminalinvalidstates each interval. - Contact-less upstream account registration —
AcmeClientService.register_accountnow acceptsemail=Noneand omits thecontactfield (RFC 8555 makes it optional; Let's Encrypt accepts contact-less registrations), so the proxy no longer blocks issuance when the only available email has a non-public TLD (.lan/.local). - ACME preflight staging order cleanup on failure — an ephemeral staging order created during a
fullpreflight is now removed from the database even when a later stage of the preflight raises, instead of leaking an orphan row. - Key-reuse renewal fallback —
key_source=reusenow falls back to the originalsource_certificate_idwhen the order's owncertificate_idis missing (e.g. an order whose import failed), so the key-reuse chain survives across renewals.
[2.185] - 2026-07-06
Added
- ACME proxy multi-CA with per-account slug endpoints — the ACME proxy upstream is now selected per external CA account (
AcmeClientAccount), so several CAs (Let's Encrypt staging, production, ZeroSSL, …) can be exposed in parallel. Each account can opt in to the proxy with a unique slug, served at/acme/proxy/<slug>/directoryalongside the legacy/acme/proxy/directory(backward compatible). Reserved slugs (directory,new-order,acct,challenge, …) prevent collisions with existing routes. Migrations050(acme.proxy.acme_account_id) and051(proxy_enabled,proxy_slug) backfill the account already linked in proxy settings. The UI surfaces a toggle and slug field on external CA accounts and lists all enabled endpoints with a Certbot example (#170). - Typed SAN validation for certificate issuance — the Issue Certificate form (
POST /api/v2/certificates) now validates SAN entries per type (DNS, IP, Email, URI, UPN) on both backend and frontend, with cross-type errors (e.g. an FQDN entered in the IP field is rejected with a hint to use DNS) and ECDSA curve mapping (256/384/521 →prime256v1/secp384r1/secp521r1). The CN auto-SAN no longer adds an email CN as a DNS SAN on server certs; email SAN is added only for Email/Combined types. New RSA 3072 size and P-384/P-521 curve options appear in the Issue Certificate selector (#169). - CI: block bot Co-authored-by trailers — a new
no-bot-attribution.ymlworkflow rejects PRs whose commits carryCo-authored-bytrailers from known AI agent identities (Cursor, Copilot, Claude, …), so commits attributed to bots can no longer reachdev.
Fixed
- ACME DNS-01 propagation diagnostics and
dns_propagation_timeout=0— the DNS-01 self-check succeeds as soon as the authoritative (or configured) resolver confirms the TXT record, but the per-public-resolver diagnostic log was emitted even on success and a flaky resolver (e.g. SERVFAIL from Quad9) was indistinguishable from a real propagation gap. Each failing public resolver is now logged at DEBUG with its exception type (NXDOMAIN/Timeout/ConnectionError/ …) and the public-propagation line is explicitly marked(diagnostic, does not block issuance).dns_propagation_timeout=0now skips the pre-check entirely on both the auto-poll background path and the manual Verify path (was a single-pass probe), matching the "submit immediately" help text (#171). - TXT RDATA multi-string concatenation — long ACME authorization tokens published as a single TXT RR carrying several
<character-string>elements (RFC 1035 §3.3.14; Quad9 splits them across quoted strings) are now joined before matching against the expected value. A correctly published record no longer renders asvalue_mismatch/pendingon such a resolver (#171). - Migration 050 NotNullViolation on PostgreSQL — the proxy account backfill inserted legacy credentials into
acme_client_accountswithoutcreated_at/updated_at; on instances where those columns areNOT NULLwithout a serverDEFAULT, the raw INSERT failed at boot (null value in column "created_at"). The migration now provides both timestamps explicitly on the SQLite and PostgreSQL paths. - Flaky OCSP/CDP auto-URL tests — CA OCSP/CDP/AIA auto-URL generation relied on
hostname -fvia_get_fqdn(), which on CI runners can return a short hostname without a domain and resolve toNone, flaking the update-CA tests. The shared test app fixture now pinsFQDN = 'ucm.test'so URL generation is deterministic.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.187
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.187
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.187Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.187/ucm_2.187_all.deb
sudo dpkg -i ucm_2.187_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.187/ucm-2.187-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.187-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.187_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation