What's Changed
Added
- HSM: a connection test records what a PKCS#11 module reports (library, manufacturer, library and firmware versions), shown on the provider's page. In FIPS mode a new SoftHSM provider is refused, since SoftHSM is not a validated module;
UCM_FIPS_ALLOW_SOFTHSM=1allows one for tests. - Users: an administrator lists another user's API keys in their details and revokes a leaked or unused one, which stops working at once; a revoked or expired key can then be deleted. Each action is audited.
- CAs: a CA can carry cross-certificates (same subject and key, another issuer) beside its own certificate: attach one, serve it in the default chain, export it, or cross-sign a held CA's key with another from the CA details; revoking one at its issuer is done through the API. Its expiry raises the usual alerts and its revocation follows the approval policies (#400).
- Chains: exports, deployments, TLS, PKCS#12, ACME, WSTEP and TSA use one chain builder that follows the served cross-certificate;
chain_variant(auto,primary,cross:<id>) picks another chain on CA and certificate exports and on a deploy binding, and EST/cacertsadds the served cross-certificates (#400). - ACME: the server offers the other chains of an order as RFC 8555 §7.4.2 alternates for
certbot --preferred-chain. AIA serves/ca/{ref}.p7cand/ca/{ref}/cross/{id}.cer, and.cerand.pemfollow the served cross-certificate (#400). - Import: a certificate that re-certifies a held CA's key is attached as a cross-certificate on a self-signed root, and on an intermediate after the Attach as cross certificate choice (
cross_mode). Smart Import attaches one next to a held root and reportscross_certificate_attached(#400). - CAs: a CA's details list the certificates and deploy bindings that still chain to it, directly or through a cross-certificate, to show when an old root can be retired;
GET /api/v2/cas/{id}/chain-usage(#400). - CAs: the chain repair bar lists records damaged before cross-certificates were modelled, with Convert to cross-certificate and Restore as primary; nothing is changed without a click (#400).
Fixed
- Import: a CA certificate re-issued by another CA over the same key overwrote the self-signed root and re-parented it. An intermediate in that case now answers 409
cross_certificate_detected, and so does installing it after Renew via CSR, with Attach as cross certificate and Replace offered; a root rollover under the same issuer name still replaces the certificate (#400). - Smart Import: a cross-certificate in a bundle replaced the self-signed root or was dropped under a generic duplicate warning depending on file order. The root is kept and the result reports
cross_certificate_attachedorcross_certificate_skipped(#400). - CAs: the hourly chain repair deleted a self-signed root when a second record shared its key. It now merges only identical certificates and reports the rest (#400).
- ACME client:
preferred_chainmatched the subject or issuer of the last certificate and preferred an alternate, so a cross-certificate could win a request for the other root. It now follows certbot: the issuer CN of the topmost certificate, default chain first (#400). - OCSP, mTLS and renewal resolved a CA through whichever record shared its key, so a cross-certificate could stand in for the self-signed root. They now prefer the self-signed record (#400).
- DEB and RPM: an upgrade stopped UCM while it rebuilt the Python environment, about five minutes with
UCM_FIPS_BUILD=1. The new environment is now built next to the running one, and the service stops only to switch to it. - UI: 17 badges asked for a size that does not exist and rendered without padding, and a few for a colour that does not exist (About, SSO tests, Smart Import, failed webhook deliveries, ACME order status). They now use defined ones, and a test refuses an unknown size or colour.
- UI: a stat card clicked to filter a list (Certificates, SSH and user certificates, Policies, Discovery, Approvals) showed no active state on desktop, so nothing said which filters were on. Active cards are outlined again.
- CSRs: the Pending and History tabs showed at most 20 requests and every count stopped at 20; both lists now page, search and sort on the server, and History also searches by signing CA (#424, thanks @clinde04).
- ACME client: an order for a domain and its wildcard kept only one of its two authorizations, which both name the base domain, so it never completed. Each now gets its own TXT value and challenge.
- ACME DNS-01: the Route 53, Azure, Gandi, deSEC, PowerDNS and NS1 providers replaced the TXT record with the new value, or refused it, so a domain and its wildcard could not both validate; they now add it to the existing values (Route 53 now needs
route53:ListResourceRecordSets, which cleanup already used). DuckDNS, Hurricane Electric and the FreeDNS provider still keep one TXT value per name. - ACME DNS-01: the Hetzner provider added a TXT value to an existing record with a PATCH the Hetzner Cloud API does not offer, so a domain with its wildcard, or a retry after a leftover value, failed. It now uses
add_recordsand keeps the record's TTL. - CAs: a root imported without its private key showed as active but was silently left out of every CA picker. It is now marked No private key with an Import private key action, and the pickers (issuing, signing, cross-signing, ACME, SCEP, EST, TSA, Windows enrollment, mTLS enrollment) say how many CAs they leave out; the API refuses a CA that cannot sign as SCEP default (an HSM-backed one too, which SCEP cannot use) or EST label, the default SCEP CA can be cleared and can no longer be deleted while set, and a policy can target any CA again (#426).
📜 Recent release history (last 2 versions)
[2.246] - 2026-10-27
Added
- ACME proxy: optional DNS UPDATE (RFC 2136) listener, off by default, so standard rfc2136 solvers can answer DNS-01 through the proxy with per-account TSIG keys (#416).
Fixed
- PostgreSQL: the web worker reused the database connections that the main process opened at startup and kept using for scheduled tasks, so the replies of both could interleave (intermittent
PGRES_TUPLES_OKerrors, rows of another query, an unhealthy container). Each worker now opens its own. - The HTTP protocol server (CDP, OCSP, AIA, HTTP-01) stopped after
systemctl reload ucmuntil the next restart, because the new worker could not take the port from the old one; it now waits for the port to be released. - ACME proxy: a challenge response now carries
Link: rel="up"even when the upstream CA omits it (Pebble), which certbot and lego require (#420, reported by @WMP). - ACME: deactivating an authorization (RFC 8555 §7.5.2), as lego does after a failed validation, got
malformedfrom the native server and the proxy. Both now accept it; the proxy deactivates the upstream authorization only when no other pending order shares it (#422, reported by @WMP). - ACME:
newAccountwith an already registered key gotexternalAccountRequired, or a refusal of its used EAB, when EAB is required. The native server and the proxy now return the existing account, as RFC 8555 §7.3.1 requires (reported by @WMP). - UI: the
danger-softandwarning-softbuttons, the red icon tiles, the outline button hover and a few tinted panels (CA details, login, reports) referred to colour tokens that were never defined and rendered without their colour. A test now fails the suite when the source uses an undeclared token.
Documentation
- New guide
docs/integrations/acme-proxy-clients.md: how to use the ACME proxy from hosts without DNS API access, with a no-op DNS-01 step for certbot, acme.sh, lego, cert-manager (stub acme-dns example inexamples/kubernetes/cert-manager/), win-acme and Posh-ACME, and the DNS resolver limits of each client (#416).
[2.245] - 2026-10-23
Security
- The forced password change now holds on the server: until the first-boot
admin, or any account asked for a new password, sets one, its session gets403for everything but the change, logout and a 2FA enrolment that is itself required, API and WebSocket included. Before, only the web UI asked, so the documented default password gave full API access to a fresh install (reported privately by @EQSTLab). - Certificate Transparency submission now goes through the pinned SSRF guard: a log URL could be redirected, or its name rebound, to loopback or a cloud metadata address after it was checked (reported privately by @fredlubrano). A log URL aimed at loopback or a cloud metadata address is now also refused when saved.
- ACME HTTP-01 and TLS-ALPN-01 validation now pins its connection under the default policy too, where private addresses are allowed: a name could answer one address to the metadata check and the cloud metadata service to the fetch (reported privately by @heidrickla).
- The PowerDNS and Plesk DNS providers now reach their server through the pinned SSRF guard: their API key, sent in a header of its own, followed a redirect to another host. A server on the same host stays reachable.
- SSH: a CA with no allowed principals signed for any name,
rootincluded. It now signs nothing unless Allow any principal is ticked (reported privately by @heidrickla). - Pillow 13.0.0 is now the minimum: 12.x carries 39 published advisories, mostly in image decoders.
- GitHub Actions are pinned to commit SHAs, kept up to date by Dependabot, and a test refuses an unpinned one.
Changed
- The forced password prompt no longer offers Skip for now or a close button: it offers a logout, and the pages load only once the password is changed, so they no longer fill the screen with refused requests behind it.
- A first-boot admin password set through
INITIAL_ADMIN_PASSWORD(DockerUCM_INITIAL_ADMIN_PASSWORD, HelminitialAdminPassword) no longer has to be changed at first login: only the public defaultchangeme123does, so automated deployments keep API access. - The packages and the Docker image no longer install pytest, black and flake8: development tools moved to
backend/requirements-dev.txt.
Fixed
- ACME proxy: with External Account Binding on, an
onlyReturnExistinglookup was refused withexternalAccountRequired, so cert-manager got no account URL and its orders failed; the lookup is now answered before the EAB check and never creates an account (#409, by @WMP). - ACME proxy: the account URL it returns now answers reads, contact updates and deactivation, along with its orders list, and
new-accountrefuses an unknown or disabled endpoint path instead of handing out an account URL under it (#410, #411, reported by @WMP). - ACME server and proxy:
new-accountrefuses a payload that is not an object, a non-booleantermsOfServiceAgreedoronlyReturnExisting, and a malformed contact withmalformedorinvalidContactinstead of a 500 or a stored value; a deactivated or revoked account getsunauthorizedinstead of its URL (#412, #413, reported by @WMP). - ACME server and proxy: every request signed by a deactivated or revoked account now gets
unauthorized(401) instead ofmalformed, and the proxy removes the new account when another registration consumed its EAB credential first, as the server already did. - ACME proxy: a stale nonce or an unusable account key now gets
badNonceorbadPublicKeyinstead ofmalformed, so clients retry a stale nonce; on both servers a kid that names no account getsaccountDoesNotExist.
Upgrade notes
- SSH: existing CAs without allowed principals get Allow any principal ticked, so they keep signing; untick it once their principals are listed. A CA created through the API without a list now signs nothing unless
allow_any_principalistrue.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.247
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.247
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.247Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.247/ucm_2.247_all.deb
sudo dpkg -i ucm_2.247_all.deb
sudo apt-get install -fFedora
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.247/ucm-2.247-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.247-1.fc43.noarch.rpmRHEL, AlmaLinux, Rocky Linux 9
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.247/ucm-el9-2.247-1.el9.noarch.rpm
sudo dnf install ./ucm-el9-2.247-1.el9.noarch.rpmFIPS mode (preview)
FIPS mode is a preview: feedback is welcome on #399. Full guide: FIPS Mode.
Docker, any Linux host (amd64, arm64):
docker run -d -p 8443:8443 -e UCM_FIPS=1 \
-v ucm-data:/opt/ucm/data -v ucm-config:/etc/ucm \
--name ucm neyslim/ultimate-ca-manager:2.247-fipsUbuntu 24.04 with Ubuntu Pro (sudo pro enable fips-updates, then reboot):
sudo apt-get install -y gcc pkg-config python3-dev python3-venv libpq-dev libxml2-dev libxslt1-dev \
zlib1g-dev libssl-dev libxmlsec1-dev libxmlsec1t64-openssl rustc-1.91 cargo-1.91
sudo install -d -m 750 /etc/ucm && echo 'UCM_FIPS_BUILD=1' | sudo tee /etc/ucm/ucm.env
sudo apt install ./ucm_2.247_all.debRHEL, AlmaLinux, Rocky Linux 9 (sudo fips-mode-setup --enable, then reboot; on RHEL, enable CodeReady Builder instead of CRB):
sudo dnf config-manager --set-enabled crb
sudo dnf install -y python3.12-devel gcc pkgconf-pkg-config libpq-devel libxml2-devel libxslt-devel \
openssl-devel xmlsec1-devel xmlsec1-openssl libtool-ltdl-devel rust cargo
sudo install -d -m 750 /etc/ucm && echo 'UCM_FIPS_BUILD=1' | sudo tee /etc/ucm/ucm.env
sudo dnf install ./ucm-el9-2.247-1.el9.noarch.rpmThe package builds its crypto libraries against the system OpenSSL (about five minutes), sets UCM_FIPS=1 on a host in FIPS mode and prints a readiness report. Fedora, Debian and other distributions are not supported in FIPS mode: use the Docker image.
Silent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.247_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation