What's Changed
Added
- ACME LOT A — shared DNS self-check for proxy and renewal — the ACME proxy and auto-renewal paths no longer use a blind fixed 30s sleep after publishing the DNS-01 TXT record. Both now poll actively via
services/acme/dns_selfcheck.py, honouringacme.client.dns_propagation_timeout(same setting as the ACME client auto-poll). If the TXT is still missing when the timeout elapses, the proxy skips upstream challenge submission and marks the challengedns_not_readyinstead of burning the token on the upstream CA. Renewal cleans up DNS TXT records on propagation or finalization failure. - ACME renewal partial TXT cleanup — multi-domain renewals now mark DNS TXT records for cleanup as soon as the first
create_txt_recordsucceeds, so a failure on a later domain no longer leaves earlier TXT records behind. - GUI toggle: verbose ACME/DNS diagnostics — ACME → Let's Encrypt → Verbose ACME/DNS logs (
acme.client.debug_logging) promotes DNS resolver diagnostics (poll ticks, per-resolver failures, lookup source) from DEBUG to INFO for troubleshooting propagation issues without raising the global log level. The flag is memoized per app context, so hot poll loops do not query the database per log line. Default off.
Changed
- ACME client DNS self-check refactor —
_dns_selfcheck/ timeout reading inorders.pynow delegate to the shareddns_selfcheckmodule used by proxy and renewal, keeping one implementation for poll interval, logging, and timeout semantics.
Fixed
- AD CS Kerberos authentication crashed on a missing unused dependency — the kerberos auth mode built the certsrv client in NTLM mode before attaching the Kerberos handler, which made the certsrv library import
requests_ntlmeven though it was never used. Every Test Connection / sign in kerberos mode failed with "No module named 'requests_ntlm'". The client is now built with a neutral placeholder auth and the Kerberos handler attached directly; all three auth modes (basic, certificate/mTLS, kerberos) smoke-tested end-to-end against a Windows Server 2025 AD CS.
📜 Recent release history (last 2 versions)
[2.188] - 2026-07-08
Security
- DNS provider credentials encrypted at rest (GHSA-38cv-3c4g-w55w) — DNS-01 provider API keys/tokens (Cloudflare, Route53, …) were stored as plaintext JSON in
dns_providers.credentialsdespite the column being labelled encrypted, so anyone with database read access (or a raw export of the field) obtained domain-control credentials. The field is now encrypted at rest via a model property (utils.encryption Fernet, mirroring the EAB HMAC key), read paths transparently decrypt (legacy plaintext rows still read), and migration052encrypts any pre-existing rows. Backup export/restore round-trips the decrypted value so cross-machine restore keeps working; regression tested (round-trip, at-rest ciphertext, constructor/restore path, migration idempotency). Reported externally by Ralph. - Dev-dependency advisories — bumped transitive build/lint tooling out of vulnerable ranges via npm overrides:
js-yaml→ 4.3.0 (GHSA-h67p-54hq-rp68, quadratic-complexity DoS in merge-key handling; pulled in by eslint) and@babel/core→ 7.29.7 (GHSA-4x5r-pxfx-6jf8, arbitrary file read viasourceMappingURL; pulled in by @vitejs/plugin-react). Both are build-time only and never shipped in the runtime bundle.
[2.187] - 2026-07-08
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.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.189
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.189
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.189Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.189/ucm_2.189_all.deb
sudo dpkg -i ucm_2.189_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.189/ucm-2.189-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.189-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.189_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation