What's Changed
Added
- Keys: ML-DSA-44, ML-DSA-65 and ML-DSA-87 (FIPS 204) are recognised by policies, CSR parsing and key labels when the linked OpenSSL provides them (3.5 or later). They are refused in FIPS mode.
- ML-DSA keys can be generated for CAs, certificates, CSRs and templates, and a renewed ML-DSA key keeps its level. Their certificates carry signature-only key usage (RFC 9881): a bit a template or CSR asked for is removed with a notice, and a digest asked for an ML-DSA signer is ignored with a notice.
- ML-DSA authorities sign their CRLs and OCSP responses without a digest, under the OID of their own level, and OpenSSL 3.5 verifies them, delegated responders across RSA and ML-DSA included.
- Policies, compliance profiles and both preflights recognise an ML-DSA signer as the
ml_dsascheme, soallowed_signature_schemesandpq_standaloneapply to what an ML-DSA authority signs. An ML-DSA key scores like the classical curve of its NIST category. - Post-quantum certificates, CAs and CSRs (ML-DSA, SLH-DSA) are shown under their own name with their security level and a Post-quantum badge, in the API, the trust store, the mTLS list and compliance grading. A key the API does not name is no longer displayed as RSA.
- Certificate chains, issuer lookups, smart import and delegated OCSP responders verify ML-DSA signatures. An unverifiable self-issued certificate whose key identifiers point at another key is no longer taken for a root.
- ML-DSA private keys written by OpenSSL in its default seed-and-expanded layout import, read from their seed after a consistency check. A key without its seed, or one whose two halves disagree, is refused with a message that says what to do.
- PKCS#12, PFX and JKS exports of an ML-DSA key answer 400 with the reason
unsupported_for_key_algorithminstead of a 500, and the export dialog greys these formats out. P7B bundles are built in-process instead of by the openssl command. - The Create CA, Issue Certificate, Generate CSR and template forms offer ML-DSA-44, ML-DSA-65 and ML-DSA-87, and Create CA hides the signature hash when the key that signs takes none. These keys are not offered in FIPS mode or on an HSM.
- Hybrid certificate pairs (RFC 9763): the Issue Certificate form and
POST /api/v2/certificateswithhybrid: {classical, pq}issue a classical certificate and an ML-DSA one whose relatedCertificate extension names it. The pair is shown linked, revokes together and renews together when the server holds both keys, unless the operator unticks the box, and exports as onehybrid-bundleZIP. - A CSR carrying a relatedCertRequest (RFC 9763) signed by the key of a certificate held here is checked before signing on Sign CSR, ACME, EST and WSTEP, and the new certificate is linked to it;
POST /api/v2/csrsbuilds one withrelated_to, which needsread:private_keysand is audited. A request is bound to the first key it is presented with, and A's chain is checked as RFC 5280 asks. A newer certificate naming the same A takes over the pair, and ACME revocation revokes only the certificate presented. Such a pair satisfies thepq_standalonerule of compliance profiles. - ACME: the local server finalizes ML-DSA CSRs, and the ACME client orders ML-DSA-44, ML-DSA-65 and ML-DSA-87 certificate keys outside FIPS mode. A default key type the client cannot generate now fails the order instead of falling back to RSA-2048.
- EST, SCEP and WSTEP enrol ML-DSA keys: SCEP verifies ML-DSA CMS signers as RFC 9882 sets out, and XCEP announces ML-DSA templates in policy schema 3 with the software KSP and a SHA-512 request hash, which Windows Server 2025 follows to enrol them. EST /serverkeygen still generates RSA-2048 and ignores the request key, ML-DSA included (RFC 7030).
- TSA: a timestamp signer can be ML-DSA-65 or ML-DSA-87 (RFC 9882), issued from the TSA page or as an ML-DSA CA signing directly.
- HSM: ML-DSA-44, ML-DSA-65 and ML-DSA-87 keys can be generated on PKCS#11 3.2 tokens, OpenBao Transit and Google Cloud KMS, and a CA on such a key signs certificates, CRLs and OCSP responses. Google Cloud KMS support follows the KMS API but is not yet tested against the service, and the forms say so; Azure Key Vault and AWS CloudHSM refuse ML-DSA with the reason.
- Compliance profiles: ANSSI, BSI and EU ACM accept ML-DSA-65 and ML-DSA-87 as the post-quantum member of a hybrid pair, never alone.
- The effective policy endpoint reports
ml_dsa, and key type forms, the ACME client's included, offer ML-DSA only on a build that has it and outside FIPS mode. The FIPS preflight reports ML-DSA CAs under a newca.unapproved_algorithmcheck and lists ACME client orders whose CSR key FIPS does not approve. - The "Certificates Only" API key scope now covers SSH certificates (
read:ssh,write:ssh) and reads SSH CAs (thanks @danielrona). - CAs: "Hide from issuance" in the CA details keeps a CA, a root typically, out of the CA lists of Issue Certificate, Sign CSR, bulk CSR signing, user mTLS certificates and the TSA signing certificate; a link under those lists shows it again (#430).
- Templates: a template, a system one included, can be deactivated from its details; the Issue Certificate form and Windows autoenrollment no longer offer it, and a CA left with no active template, its pinned ones all deactivated included, offers none. A system template still cannot be edited: duplicate it to change it.
Changed
- CRL settings: the CRL digest of an Ed25519, Ed448 or ML-DSA authority is reported as empty and setting one answers 400; the CRL page only offers to clear one saved before.
- Issuance: an unknown
keyAlgoonPOST /api/v2/cas, or an unknown key type on a certificate, CSR or template, now answers 400 instead of falling back to RSA-2048. SLH-DSA CSRs are refused as not signable by this instance. - SSH CA routes now require the new
read:ssh_cas,write:ssh_casanddelete:ssh_casscopes, sowrite:sshno longer creates or edits SSH CAs. Upgrade note: roles, groups and API keys holding an SSH scope receive the matchingssh_casscope on upgrade and when an older backup is restored. - FIPS mode: the ACME client, EST, SCEP and WSTEP refuse a CSR key FIPS does not approve (RSA under 2048 bits, Brainpool, EdDSA, ML-DSA, SLH-DSA) with a message that names FIPS, before checking the CSR signature.
Fixed
- OCSP: a CA whose certificate carries a compressed EC point is found by its issuerKeyHash instead of answering unauthorized, and the responder ID hashes the key as the certificate encodes it. A multi-certificate request signed by an ML-DSA responder no longer fails.
- CSRs: re-keying or cloning a CSR turned an EC P-384 or ML-DSA request into RSA-2048 or P-256; the request now keeps its key type.
- ACME accounts: the account detail named every key RSA-2048; the list and detail now return the key the account's JWK holds.
- Generated CSRs: an Ed25519 or Ed448 key was signed with the requested SHA-2 digest and failed; keys whose scheme takes no digest are now signed without one.
- CA details and renewal with a new key: an algorithm without a size or curve no longer falls back to the first algorithm of its family.
- Smart import: an encrypted private key was kept in its encrypted form and silently dropped on import; the decrypted key is now matched and stored. A wrong key password now stops the import with its reason instead of importing the certificate alone, and a key given with a password it does not need is read.
- Key uploads, the key matcher and the converter: a malformed key now answers 400 with its reason instead of a generic error, or of a 200 that returned the certificate alone.
- Generate CSR now offers RSA-3072, and re-keying an RSA-3072 request keeps that size. Create CA's "Use existing key" lists only the HSM keys the policies, the compliance profile and FIPS mode allow.
- ACME client settings now flag a saved certificate key type this server no longer offers, instead of showing an empty field.
- Template preview: an RSA-2048 template was shown as "RSA-2048 256-bit"; it now shows the key's name.
- ACME DNS-01 with GoDaddy: TXT records were written with a PATCH the API does not offer, and removed with an empty PUT it refuses. The values of a name are now read, merged and written back with PUT, and cleanup uses DELETE.
- ACME DNS-01 with deSEC: a new TXT record used a 300 s TTL, below the 3600 s minimum deSEC sets on each domain, and was refused. The domain's minimum TTL is now applied, and apex records are cleaned up.
- ACME DNS-01: the ACME client, renewals and every cleanup now take the per-name lock the proxy uses, so two writers of one
_acme-challengename no longer drop each other's value. - ACME DNS-01 with PowerDNS: a value already present but disabled was added a second time, which PowerDNS refuses; it is now re-enabled. With Route 53, an alias or routing-policy TXT record is no longer overwritten or deleted, and the existing set is found whatever the case of the name.
- CAs: when the hourly check merged two records of one CA, the SCEP, EST, XCEP, WSTEP, TSA, mTLS and ACME settings that named the removed record kept pointing at it. They now follow the kept record, with the SCEP challenge and the CRL and OCSP publication flags, and the removed record's URLs resolve to it; EST labels also follow a duplicate converted into a cross-certificate.
- SCEP: an EC or other non-RSA CA was accepted as the default CA and on a profile, although SCEP clients encrypt their request to an RSA key, so every enrolment failed. It is now refused and left out of the pickers; one already saved is reported as not usable on the dashboard, and the SCEP endpoint says why.
- Settings: the mTLS trusted CA picker hid CAs without a key, although the trusted CA is the anchor of client certificates; it now lists every CA whose certificate is not revoked, keyless or offline included, and keeps the saved one. The ACME domain form no longer puts a space between two Japanese or Chinese sentences.
- API: an HTTP error without a dedicated handler, such as the login attempt limit or a request whose body is not JSON, answered 500 with a stack trace in the log; it now keeps its own status (429, 415...) and headers. A POST to
/api/v2/auth/methodsanswers like the GET, whatever its body. - Behind a reverse proxy that sent
Connection: upgradeon every request, a request with a body, such as the login, hung until the proxy timed out: the server took it for a WebSocket handshake. Only a request withUpgrade: websocketis now treated as one. - ACME server: revoking a certificate with its own key, when that key cannot sign a request (EdDSA, Brainpool, ML-DSA), answered a bare 403; the answer now says to revoke with the ordering account.
- Certificates issued through ACME, SCEP, EST, WSTEP and Microsoft CA connections, or renewed, were stored without their key identifiers, or in lower case, so ACME renewalInfo answered 404 and the issuing CA was not found by them; they are now recorded with the certificate.
- Import: a certificate or CA with an encrypted private key and a wrong or missing password, or with a key that could not be read, was imported without its key; the import now answers 400 with the reason. Key uploads say whether a password is missing or wrong, and smart import tries every per-file password the form sends instead of ignoring them. Smart import also ignored the objects left unselected and the Import keys option, and now offers Import CSRs.
- WSTEP: renewing a certificate from Windows (
certreq ... Renew) was refused for lack of a WS-Security header; Windows authenticates with the certificate being renewed as TLS client certificate, which is now accepted when it signed the request, is current and was issued by the WSTEP CA. It needs mTLS enabled with that CA, or one of its ancestors, trusted. - WSTEP: a renewal could move to another template and add its Extended Key Usages; it now keeps the template of the certificate it renews with that template's EKUs, and a renewal wrapped in PKCS#7 with a new key has to prove possession of it.
- EST, SCEP and WSTEP: two renewals of one certificate sent at the same time, or two pending SCEP renewals approved in turn, could both be issued; the second is now refused as superseded, and a SCEP renewal approved after the certificate was revoked is refused. WSTEP renewals of a certificate the CA issued, refused ones included, are audited with the old and new serial numbers.
- WSTEP: a request matched to a template by its EKUs could be issued with a key of another kind than the template's; it is now refused with the template's key type.
- HSM: AWS CloudHSM providers accepted Brainpool keys the module cannot generate and failed with a 500; they now answer 400 with the reason. OpenBao no longer signs with a key type it cannot name, and a Google Cloud KMS key version without a digest is refused instead of signed with SHA-256.
- CSRs: re-keying a Brainpool or ML-DSA CSR in FIPS mode sent a key type the server refuses; the form now falls back to the first key type it offers.
- Certificates: a direct
/certificates/{id}link closed its detail window at once when "Close on navigate" was on. - Certificates: the row actions of the certificate list (export, rename, renew, revoke, delete) had disappeared, which left renaming unreachable; they are back with the detail view's permissions and revocation dialog, and renaming is also offered in the detail views.
- WebSocket: SSH certificate events reached every holder of
read:certificates; they now reach holders ofread:sshonly.
Security
- dnspython 2.9.1 (CVE-2026-114605): a DNS message with a long chain of compression pointers, sent to the RFC 2136 listener or returned by a resolver, could hold a CPU. The listener reads compressed names the same way under the new version.
- ACME server:
revoke-certsigned with a certificate's own key found the stored certificate by serial only, so a self-signed certificate forged with the serial of an issued one revoked it. Revocation now acts only on the stored certificate, byte for byte. - Smart import and the format converter: an encrypted key or PKCS#12 file, including a key sealed in an inner encrypted layer, could ask for any number of key-derivation iterations. Costs above 2 million PBKDF2 or PKCS#12 MAC iterations, or scrypt above N=2^20, are now refused without being run, and a request makes at most 64 password attempts. Certificate and CA import, key uploads, the decoder, the key matcher and offline CA restore apply the same limits.
📜 Recent release history (last 2 versions)
[2.247] - 2026-10-30
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).
[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).
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.248
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.248
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.248Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.248/ucm_2.248_all.deb
sudo dpkg -i ucm_2.248_all.deb
sudo apt-get install -fFedora
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.248/ucm-2.248-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.248-1.fc43.noarch.rpmRHEL, AlmaLinux, Rocky Linux 9
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.248/ucm-el9-2.248-1.el9.noarch.rpm
sudo dnf install ./ucm-el9-2.248-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.248-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.248_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.248-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.248_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation