What's Changed
⚠️ Upgrade note — LDAP/SSO certificate verification is now enforced. Before this
release, an LDAP/SSO provider with "verify SSL" enabled but no CA bundle uploaded
silently performed no certificate validation. It now validates the LDAP server
certificate against the system trust store. Action required: if your LDAP server
uses a private or self-signed certificate that is not in the host's system trust store,
upload its CA as the provider's CA bundle before upgrading, or those logins will start
failing with a TLS validation error. Providers with "verify SSL = off" are unaffected.
See #181.
Security
- LDAP TLS was not validated when "verify SSL" was on but no CA bundle was set — with SSL verification enabled but no CA bundle uploaded, the LDAP/SSO TLS builder fell back to ldap3's default TLS, which does not validate the server certificate, so a "verify SSL = on" provider silently performed no verification (MITM exposure on LDAP auth). It now validates against the system trust store (
CERT_REQUIRED). Note: verification is on by default — an LDAP server using a private/self-signed certificate that is not in the system trust store must now have its CA uploaded as the provider CA bundle (previously such setups connected without validation). Explicit "verify SSL = off" is unchanged. Contributed by @heidrickla (#181). - SSRF guard could be bypassed via unspecified and IPv4-mapped IPv6 addresses — the outbound-URL guard (webhooks, SSO discovery, ACME proxy) checked loopback but not the unspecified address (
0.0.0.0/::, which route to loopback on most systems), and matched the cloud-metadata deny-list by string, so an IPv4-mapped IPv6 encoding (e.g.::ffff:169.254.169.254) slipped past. The guard now collapses IPv4-mapped IPv6 to IPv4, compares against a parsed-IP deny-set, and treats unspecified addresses as forbidden. Contributed by @heidrickla (#182). - Outbound request hardening: default timeout + DNS-rebinding fix — the SSRF-pinned request helpers sent no default
timeout, so a stuck upstream could hang a worker indefinitely; they now default to 30s (overridable). The ACME proxy connection test validated the host and then fetched viaurllib.urlopen, which re-resolved the hostname independently (DNS-rebinding window); it now fetches through the pinned SSRF-safe helper so resolution, deny-list re-validation, and the pinned connection all use the same IP. Contributed by @heidrickla (#183).
Changed
- SCEP crypto migrated off the unmaintained pyCrypto to pyca/cryptography — the SCEP crypto helpers and message parser now use
cryptography's ciphers (AES-256-CBC, 3DES-CBC) instead ofpycryptodome, which is dropped as a dependency. The algorithms are standard so ciphertext is byte-identical and existing SCEP clients' messages keep decrypting unchanged. Contributed by @heidrickla (#184).
Fixed
- SCEP auto-approve enrollment crashed on a naive/aware datetime comparison — when clamping an issued certificate's validity to the CA's expiry, the code compared
utc_now()(naive-UTC) against the CA certificate's timezone-aware expiry, raisingcan't compare offset-naive and offset-aware datetimes. The SCEP handler masked it as a generic failure, so every auto-approved enrollment failed with "Internal SCEP processing error" (the renewal validity-window check had the same flaw). Both comparisons are now done consistently, with a regression test that issues against an online CA. Verified end-to-end with a real SCEP client (enrollment now returns SUCCESS). api.v2failed to import on non-POSIX platforms —api/v2/system/https.pydid an unconditional top-levelimport pwd(a Unix-only stdlib module), and sinceapi/v2/__init__.pyimports every submodule eagerly, the wholeapi.v2package (and the test suite) failed to import on Windows withModuleNotFoundError: No module named 'pwd'.pwdis now imported lazily inside the twochownsites and skipped gracefully off POSIX; Linux behaviour is unchanged. Contributed by @heidrickla (#179).- Removed unreachable dead code in the backup service —
BackupService._encrypt_private_keyhad areturnafter its realreturn, dead code that would have leaked the plaintext key if ever reached. Contributed by @heidrickla (#180).
📜 Recent release history (last 2 versions)
[2.190] - 2026-07-10
Fixed
- API-key creation rejected valid permission resources — the permission validator in
POST /api/v2/account/apikeysused a hardcoded resource list that had drifted from the scopes actually enforced by the API: keys could not be scoped tocsrs,user_certificates,templates,truststore,est,hsm,ssh,policies,approvals,key_recovery,audit,groupsorsso(e.g.write:csrs, required to sign a CSR, was rejected — leaving CSR signing reachable only with a full-access*key). The valid set is now derived fromROLE_PERMISSIONSplus the admin-only resources (users,system,sso), with a regression test that scans every@require_authscope so the validator can no longer drift. Contributed by @heidrickla (#178).
[2.189] - 2026-07-09
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.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.191
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.191
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.191Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.191/ucm_2.191_all.deb
sudo dpkg -i ucm_2.191_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.191/ucm-2.191-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.191-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.191_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation