๐ Recent release history (last 2 versions)
[2.206] - 2026-08-08
Added
- Optional purge of replaced ACME-proxy certificates (#240) โ new opt-in ACME setting
acme.proxy.prune_replaced_certificates(UI toggle under ACME โ Let's Encrypt proxy): when a proxy order finalizes, certificates previously imported by proxy orders for the exact same domain set are deleted instead of piling up in the inventory. Revoked certificates are always kept, and certificates not issued through the proxy are never touched. Off by default. (Note: the proxy never stores the client's private key in the first place โ keys stay client-side by design of ACME CSRs.) - CRL validity window extended to 5 years for offline CAs (#236) โ
validity_daysnow accepts 1โ1825 (was 1โ365) and the CRL/OCSP page offers 90d/180d/1y/3y/5y options, with a warning past one year since relying parties may keep stale revocation data for the full window. Intended for offline root CAs that cannot re-sign CRLs on schedule; online CAs should stay at short validity.
Fixed
- Duplicate CA/certificate creation on double-submit (#269) โ the Create CA modal and Issue Certificate form never disabled their submit button, so a double-click (or Enter+click) during a slow RSA keygen created two identical objects. Both forms now ignore re-entrant submissions and disable the button while one is in flight.
[2.205] - 2026-08-07
Security
- ACME proxy endpoints now verify resource ownership โ the proxy's authorization, challenge, order-status and certificate-download endpoints verified the JWS signature but never checked that the requesting account owned the resource: any account registered on the proxy could read any other account's authorization status and challenge details, poll their orders, and download their certificates by enumerating the base64-encoded upstream identifiers (the native ACME server was not affected โ it has enforced this binding all along). Every order-scoped proxy endpoint now enforces the owner binding recorded at new-order (account id and/or client JWK thumbprint), the same check finalize already performed: a foreign account gets
unauthorized(403), and an owner-bound resource whose requester identity cannot be derived fails closed. Because challenge and authorization URLs live in disjoint namespaces on most CAs, a challenge is attributed to its order through the authorization URL the upstream CA itself returns in theLink rel="up"header, never by guessing from the URL shape. Resources no longer tracked by any proxy order (e.g. the order row was deleted) now return 404 instead of being fetched upstream; orders created before ownership tracking existed carry no binding and are served as before. Reported by @gb-123-git (#260). - Trusted-proxy gate vs ProxyFix
REMOTE_ADDRrewrite โ withUCM_BEHIND_PROXY=1(orUCM_TRUSTED_PROXY_HOPS>0), Werkzeug's ProxyFix rewritesREMOTE_ADDRfromX-Forwarded-Forbefore the trusted-proxy gate ran. Two consequences: requests arriving through a configured reverse proxy failed the check (every nginx-proxied Web UI request was 403'd with "Untrusted X-Forwarded-Host from non-proxy client", and mTLS / EST client-cert headers from the proxy were ignored); and a direct attacker who could reach the backend could pass the gate by sendingX-Forwarded-For: 127.0.0.1, unlockingSSL_CLIENT_*/X-SSL-Client-*header handling used for client-certificate authentication. Trust decisions now key on the real TCP peer preserved inwerkzeug.proxy_fix.orig(utils.trusted_proxy.immediate_peer_addr()),client_ip()trusts ProxyFix's hop-aware result when it rewroteREMOTE_ADDRinstead of re-parsing client-supplied XFF entries (the legacyX-Real-IPfallback is kept when ProxyFix found nothing to apply), spoof-attempt warning logs report the real peer, and the mTLS middleware reuses the shared gate instead of its own duplicated (equally affected) check.
Fixed
- Navigation highlight now derives the active item from the URL path โ the active page was derived from the first URL segment under a "segment === nav item id" naming convention, so every route that breaks the convention never highlighted and its sidebar group never auto-expanded:
/key-recovery(nav idkeyRecovery) and the SSH pages/ssh/casand/ssh/certificates(nav idsssh-cas/ssh-certificates) were permanently unhighlighted, whilescep-config,est-configandtsa-configonly worked through a hand-maintained exception map. The active item is now resolved by matching each nav item'spath(exact match first, then longest prefix so detail pages like/cas/123still highlight their section), which keeps the highlight correct by construction for any future nav item. The Key Recovery contextual help panel, gated on the same broken id, now opens as well. - EST settings no longer reject a fresh CA selection when the previously configured CA was deleted โ deleting a CA leaves its
est_ca_refidvalue behind, andGET /api/v2/est/configkeeps returning that stale refid (reportingca_id: null); the settings page PATCHes the whole object back, so the stale refid arrived alongside the freshca_idand the update handler, which validatedca_refidfirst, answeredCA not foundbefore ever looking at the valid selection. When both fields are sent,ca_idis now authoritative; aca_refid-only request is validated exactly as before. - SSH CA TTL fields accept the duration format the UI advertises โ the create/edit placeholders say "e.g. 24h, 7d, 365d", but
default_ttl/max_ttlwere cast with a bareint(), so365dwas rejected withinvalid literal for int()(and the import path stored the raw string, crashing later at issuance). All three entry points (create, update, import) now normalize throughutils.duration.parse_duration_seconds()โ plain numbers stay valid as seconds,s/m/h/d/w/ysuffixes are accepted, and malformed values fail with a clear 400 naming the accepted formats. - Approved issuance (policy workflow) now honors the certificate template โ when a deferred certificate request finalized after approval, the legacy approval issuance path ignored the template's Key Usage / Extended Key Usage, used hardcoded cert-type profiles, always signed with SHA-256 (ignoring the template digest), fell back to its own request defaults instead of the template's key type/validity, stored the resulting private key unencrypted, dropped the
template_idlinkage, and skipped the CA-expiration sanity check. The approval path now applies the template's extensions template / digest / defaults like the direct issuance path (#226), encrypts the issued private key viaencrypt_private_key, preserves the template link withtemplate_overrides(#258), and refuses validity beyond the CA's own end-of-life.
Added
- Custom Command DNS provider for ACME DNS-01 โ a new DNS provider type (
custom_command) running admin-configured local commands for TXT record create/delete, with record details passed through environment variables (DOMAIN,RECORD_NAME,RECORD_VALUE,TTL,ACTION). This is the escape hatch for DNS providers not natively supported (#249 โ e.g. nicmanager or any of lego's 220 drivers reached through a small wrapper script); lego itself ships no record-level CLI, so a direct binary call was not possible. Hardened: absolute binary path required, no shell (argv-split, no pipes), hard timeout (5โ300 s, default 60), output truncated in error reports. The command binaries must exist and be executable or the provider test reports it. - dns-persist-01 ACME challenge support (draft-ietf-acme-dns-persist-01) โ the built-in ACME server can now authorize DNS identifiers via a persistent TXT record at
_validation-persist.<fqdn>bound to the requester's ACME account (accounturi) and an issuer-domain-name, so clients renew without writing DNS records. Opt-in under ACME settings, off by default: persistent validation ties issuance capability to the account key for the record's lifetime (draft ยง7.2), and the UI states this on toggle.policy=wildcardis honored for wildcard/subdomain scope (draft ยง5/ยง6),persistUntilrejects new validations past its timestamp, malformed records produce ACMEmalformederrors, account mismatchesunauthorized. Issuer-domain-names come from the configured CAA identifiers, falling back to the ACME public hostname. Challenges still expire per the normal ACME authorization lifecycle. acme.dns01_nameserversacceptshost:portentries โ the optional authoritative-resolver override (used by DNS-01 and dns-persist-01 validation) can now point at resolvers not listening on port 53 (e.g. a loopback-only BIND or a dnsmasq instance on an alternate port). Comma-separated as before; plain IPs still work.- Certificates issued from a template now record how they diverged from it โ key type, validity and digest are template defaults that a request may legitimately override (issue #226 shipped that behavior); the issued certificate keeps its template link and now records which of those values were explicitly overridden (#258, option 2 debated there). The certificate detail shows the template name with a "modified from template" indicator listing the divergent fields, and the certificate list gains a Template filter to find diverged certificates. The record is written at issuance and frozen โ editing the template later does not rewrite history. The value is exposed as
template_overrides(list of field names) in the certificate API responses; renewals at par keep it.
Fixed
- mTLS reverse-proxy certificate headers never honored โ the mTLS middleware,
login_mtls(),/api/v2/auth/methods, and/api/v2/mtls/enrollall matchedX-SSL-Client-*names againstdict(request.headers), whose keys WSGI reconstructs title-cased (X-Ssl-Client-Verify), so the exact-caseinchecks never matched and client-certificate authentication through nginx/apache proxy headers silently never fired (the designed spoof-attempt warnings were equally dead). All four sites now userequest.headersdirectly, which is case-insensitive.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.207-rc1
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.207-rc1
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.207-rc1Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.207-rc1/ucm_2.207.rc1_all.deb
sudo dpkg -i ucm_2.207.rc1_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.207-rc1/ucm-2.207.rc1-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.207.rc1-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.207.rc1_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation