github NeySlim/ultimate-ca-manager v2.251

one hour ago

What's Changed

Added

  • CAs: Ed25519 and Ed448 keys can be generated for a root, intermediate or external CA, and an EdDSA CA key imported with or after its certificate (POST /cas/import, certificate import with its key, POST /cas/<id>/key) is accepted instead of a 400 (#436, reported by @Faraphel).
  • Certificates: templates and the issue form can add custom X.509 extensions, each an OID, a critical flag and its DER value in hex, and the certificate and CA details list the extensions UCM has no section for, with their raw value (#437, requested by @Faraphel).

Security

  • Database: on PostgreSQL, an error carried the values bound to its statement, private keys included, into the logs despite hidden parameters: in the quoted statement, in the refused row of a NOT NULL or CHECK violation, and in the input a data error quotes. These are now removed from the message; its first line, the other DETAIL lines, HINT and SQLSTATE are kept.
  • Revocation: a key revoked as compromised was kept only for a certificate of a CA held here, so a Let's Encrypt certificate revoked as compromised then deleted let the ACME client certify the same key again. Such keys are now kept whatever CA issued the certificate, and refused at the request, the renewal and the finalization (migration 129).
  • Key recovery: a request named its certificate by number, and SQLite gives a deleted certificate's number to the next one created, so an approved request could export another certificate's private key, after a replacing restore in particular. Approval and recovery now answer 410 unless the number still names the certificate the request was opened on, and a replacing restore cancels the open requests on the certificates it removes.

Fixed

  • XCEP: an ECDSA template was announced without its key algorithm, so Windows generated an RSA key of the curve's size and the enrolment failed. The template now names its curve (ECDSA_P256, ECDSA_P384, ECDSA_P521) as a signature key.
  • WSTEP: two templates sharing an EKU were told apart by their id alone, so a P-384 request was issued under the P-256 template and stamped with its OID. The request key must now fit the template: same curve or ML-DSA set, an RSA key at least as long.
  • WSTEP: the CMC envelope Windows signs with an ECDSA key was refused, because CertEnroll names the key type (id-ecPublicKey) as its signature algorithm. It is now verified with the digest the envelope declares.
  • Database: on PostgreSQL in a schema other than public, the integrity check reported 0 tables. It now counts the tables of the schema in use.
  • Database migration: to PostgreSQL in a schema other than public (a search_path in the URL), the sequences of the copied tables were never found, so the migration was refused at its verification. The sequences of the schema in use are now realigned, and the reset the failure message asks for names that schema.
  • Database: on PostgreSQL, the database reset waited forever for the transaction of its own request. It now closes that transaction before dropping the tables.
  • Renewals: a renewed OCSP responder certificate was signed with the policies' default digest instead of the original's, and a renewal that changed the digest said nothing. Both keep the original's while it is at least the CA key's and the policies allow it, else follow the issuance rule, and log the change (#397).
  • XCEP: an ECDSA template outside FIPS mode announced no request hash, and a P-384 or P-521 template asked for its template digest, SHA-256 by default. ECDSA templates are now schema 3 and announce the stronger of the template digest and the one matching their key; RSA templates outside FIPS mode stay schema 2, whose hash MS-XCEP requires to be nil (#397).
  • EST and mTLS: server-generated keys were always RSA-2048, so a policy refusing it (CNSA, strict FIPS profile) refused EST serverkeygen and user mTLS certificates. The key now takes the issuing CA's strength, in the family of the serverkeygen request's key when RSA or EC, else the CA's (a browser-usable type for mTLS), and moves to the closest type the policies allow (#397).
  • Issuance: EST, SCEP, WSTEP, Sign CSR, TSA signer and unconfigured ACME orders signed with SHA-256 whatever the CA key, and a P-384 CA's CRLs too. Every path now signs with the bound template's digest, raised to the one matching the CA key (SHA-384 for P-384, SHA-512 for P-521), as a preference that yields to the closest stronger digest the policies allow, or the strongest allowed (#397).
  • Docker: the images lacked pyspnego's Kerberos backend, which the DEB and RPM installs can add, so the Kerberos binding of Windows autoenrollment could not work in a container. Both images now ship it, at the pyspnego version the requirements resolve (#435, thanks @JPromi).
  • Restore: two DNS providers, ACME CA accounts or webhooks of the same name were merged into one row, and one credential was lost. A clashing row is now restored as name (2), and the restore result lists it; an account is matched by its account URL and a webhook by its URL first.
  • DNS providers, ACME CA accounts and webhooks: the API accepted a name already in use. It now answers 409, on creation and on rename.
  • ACME client: a renewal imported its certificate into a new row, so the HTTPS binding, deployment bindings and webhooks stayed on the old one, which revoke_on_renewal then revoked. The certificate is now renewed in place, keeping its id, and certificate.renewed is emitted.
  • ACME client: the key of a finalization lived only in memory, so a restart or a failed import after the CA had issued lost the certificate. The key is now stored encrypted on the order before the request, and a status check, a finalization or the next renewal imports the certificate the CA already issued (migration 123).
  • ACME client: a manual and a scheduled renewal of one order could run at once and obtain two certificates. Each renewal or finalization now holds a lease on its order, which a stopped process leaves for 10 minutes: a second one gets a 409, and the scheduler skips the order.
  • ACME client: after 5 failed scheduled renewals an order was never renewed again, silently. It is now retried once a day, each failure emits the acme_order.renewal_failed webhook event, the order list shows the failure count and last error, and the scheduler task reports a warning.
  • ACME client: an order whose CA account had been deleted renewed under another account, possibly at another CA. The renewal and the order's other actions now stop with a message saying what to do, and deleting or deactivating an account is refused (409) while issued orders renew through it; each order's automatic renewal can now be turned off from the order list (PATCH /api/v2/acme/client/orders/<id>).
  • ACME client: a CSR whose key was revoked as compromised was refused only at finalization, after the CA had opened an order and checked the challenges at each renewal. It is now refused with a 409 by the request and before a renewal opens any order.
  • ACME client: a failed manual renewal answered a bare 500 and was neither counted nor audited. It now answers 409 or 422 with the reason, counts towards the order's failures and emits acme_order.renewal_failed.
  • ACME client: an order the CA answered 404 or 410 for was polled until the timeout, up to several minutes per scheduler pass. Polling now stops at once and the order turns invalid with the reason.
  • ACME client: a restart during the automatic validation left the order processing for good. Such an order now goes back to pending with a message, ready for Verify, within about 15 minutes of its last activity (at startup, then by a scheduler task every 15 minutes), so the orders another instance on the same database is validating are left alone.
  • ACME client and proxy: removing a DNS-01 TXT record deleted every value of its name, so cleaning up one order could erase the challenge of another order, or of the wildcard and apex sharing the name, still validating. DNS providers now remove only the value of that order; DuckDNS and Hurricane Electric, which hold one value per name, and FreeDNS, whose record list truncates TXT values, keep clearing the name and log a warning, and FreeDNS no longer removes the _acme-challenge record of another domain of the account.
  • DNS providers: Namecheap found no host in its API's answers, which carry an XML namespace, so publishing a TXT record replaced every record of the zone with it. The hosts and the mail setting (EmailType) are now read and written back, and nothing is written when the read fails.
  • ACME client: the order list loaded all its pages at once, so a rate limit answering 429 to one of them left the page empty. Pages now load four at a time, one answered 429 is retried after its Retry-After, and a page still missing leaves the orders obtained with a notice saying the list is partial.
  • FIPS mode: Smart Import and the converter accepted a file holding an Ed25519 or Ed448 key, which the other imports refuse, and every import let through an EdDSA X.509 certificate. They now answer the same FIPS refusal before parsing; an encrypted PKCS#8 key can only be refused once decrypted.
  • ACME client: the order list stopped at the 100 newest orders, so an older issued order disappeared from the page. The API now pages (page, per_page) and the page loads every order.
  • ACME client: the TXT records of a DNS-01 order whose automatic validation failed stayed in the zone. They are now removed once the order is invalid or its finalization failed; an order left pending keeps them for Verify.
  • ACME client: deleting a certificate after revoking it did not stop its order, and the next pass issued a new one. The order's automatic renewal is now turned off, with a message saying so.
  • ACME client: a renewal used a DNS provider that had been disabled, and an order reusing the key of a deleted certificate silently got a new key. Both now stop before the CA with a message.
  • ACME client: the help of the renewal threshold did not say that a certificate whose life is shorter than 1.5 times the threshold renews in its last third. It now does.
  • ACME client: with revoke_on_renewal, a revocation the CA refused or never received was only logged, and a certificate revoked here as compromised was revoked at the CA as superseded. The CA now gets the local reason when it is keyCompromise, affiliationChanged, superseded or cessationOfOperation and none otherwise, and a failed revocation is retried by the renewal task until the certificate expires, emitting acme_order.revocation_failed.
  • Revocation: a key revoked as compromised was forgotten once its certificate was deleted, so the ACME client could certify it again. The revocation record, written when the issuing CA is held here, now keeps the key's SHA-256 (migration 128).
  • Restore: a membership, deployment binding, CRL binding, template pin or role permission was matched by the source's row numbers first, and could overwrite another row here. Rows are now matched by what they name; an archive records the database it comes from, and its numbers count only there and only when they still name the same rows.
  • Restore: a replacing restore failed on PostgreSQL as soon as a CRL, a cached OCSP response, a session or an ACME client order pointed at a row it removed, and left such rows pointing at nothing on SQLite. Both engines now clear regenerable rows and queues, detach or drop history as deleting the row does, empty the optional links the schema does not enforce, and refuse any other link with the row named.
  • Restore: a replacing restore wrote the archive before removing what it does not carry, so a SCEP slug or a name still held by a removed row failed with a bare 500. Removals now come first, a homonym replacing the row here keeps its name unless a kept row still points at that row, in which case it is restored as name (2), and a remaining database conflict names the table and the row.
  • Restore: an archive over 50 MB was refused with a 413 although it had been created without a word. The restore upload now accepts and reads up to BACKUP_MAX_UPLOAD_MB (512 by default), refusals name the setting, and creating an archive a restore would not read warns.
  • Restore: an archive older than 2.230 holding an API key or an ACME domain was refused whole, its nested group members went to whichever account held the source's number, and its nested WebAuthn keys were ignored. A row whose required link is a bare number, nested members included, is now left out and listed, and the WebAuthn keys come back.
  • Restore: a hybrid certificate pair came back with the source's number for its partner, which on another installation named another certificate or none. The partner, and a SCEP request's profile, are now found by name; for any link an older archive carries as a bare number, a row here keeps its own link unless the archive records none or the restore removes the target, and a new row is restored without it, with a warning in the result.
  • Restore: an HSM field an older archive holds under another installation's key was dropped with a log line that did not name the provider. The log and the restore result now name the provider and the field.
  • Backups: ACME client orders, approval requests and key recovery requests were in no archive, so a restore lost the renewal schedule and every pending request. They are now carried, an order with its pending key and its renewal link, and a recovery request follows its certificate's refid and is cancelled when that certificate is not there.
  • Restore: settings naming a row by its number (the SCEP and ACME issuing CAs, the ACME public TLS certificate and proxy account, OCSP responders, SCEP challenges, SCTs, cross-certificate URLs) kept the source's number, so SCEP or ACME could sign with another CA. The archive now names those rows, the restore renumbers the settings, and one whose row is not there is emptied or left out with a warning. A replacing restore also drops the settings of the rows it removes, which SQLite would have handed to the archived row taking their number.
  • Restore: a failed restore left on disk the certificate and plaintext key files of the authorities and certificates it had rolled back, and a replacement left those of the rows it removed. Files are now written, and those of removed rows deleted, only with the commit.
  • Restore: a client certificate was found again by its serial number, unique only per issuer, so the same certificate could come back twice and another one with that serial be overwritten. It is now found by its fingerprint, as the archive identifies it.
  • Restore: ACME accounts and EAB credentials came back dated from the restore, and webhooks without their authentication token. Both now come back as archived.
  • Restore: a value longer than its column failed on PostgreSQL with a bare 500. It is now refused with a 400 naming the table, the row and the column.
  • Backups: a manual backup could read the database while a restore was committing and archive half of each. Backups and restores now share one lock: a restore is refused (409) while a backup runs, and a manual backup waits for a restore.
  • Restore: an archive older than migrations 101 to 105 put back what they had cleaned (legacy EAB keys, dead and ldap_* settings, SAML providers signing requests, group permissions stored as text). Those repairs are now replayed on the rows the restore writes.
  • CAs and certificates: deleting a CA left its SCEP challenge and OCSP responder settings, and deleting a certificate its SCTs, which SQLite could hand to the next row given that number. They now go with the row.
  • Database migration: a role inheriting from a role created after it made the copy to PostgreSQL fail when the database role cannot disable foreign keys. A link to a row of the same table is now written once the whole table is in.
  • mTLS: a client certificate could not be enrolled, imported or assigned when an enrolled certificate of another CA had the same serial number, legitimate in X.509. An enrolment is now identified by its issuer and serial, at login and by a restore too (migration 130).
  • Backups: a webhook's authentication token was archived twice, in the clear and encrypted under the source's key, which no other installation can read. The archive now carries it in the clear only, and a restore ignores the encrypted copy of an older archive.
  • Restore: rows sharing an identity the database does not keep unique, such as two revocation records of one serial, were matched to the archive at random, and a replacing restore silently kept the extra ones. Archived rows now go to them by rank, oldest first, a row without a complete identity is matched to none (the dedicated restorers still match one with any part of it set), a replacement removes those the archive does not carry and lists them in the result's warnings, and client certificates, discovered certificates and SCEP requests are identified by issuer and serial, endpoint, and transaction and CA.
  • DNS providers: Constellix called v1 record paths on the v4 API and added a second record for a name already holding a TXT value. It now uses the documented v4 records endpoint, reads every page, waits for an accepted creation to be listed, and adds or removes a value inside the name's single record, keeping disabled values disabled.
  • DNS providers: Checkdomain deleted TXT records through an endpoint the API does not have, by a record id it does not return, and read only the first page of domains and records. A value is now removed by replacing the zone's records without it, as the API documents, after reading every page; only the SOA, the apex NS and the generated www copies are left out.
  • DNS providers: Core Networks called zone and deletion endpoints the API does not have, logged in with a form instead of JSON and ignored a failed commit, so cleanup reported success with the record still published. It now uses the documented /dnszones endpoints, deletes one value through records/delete and fails when the commit fails.
  • DNS providers: Hostinger called an API host and record endpoints that do not exist. It now uses the documented /api/dns/v1/zones endpoints, appending a value on creation and rewriting the name's other TXT values, disabled ones included, on deletion.
  • DNS providers: Epik sent records in a shape the API rejects, read the listing with field names it does not use and deleted through a path it does not have, so no TXT record was ever removed. It now follows the documented v2 host-records calls and sends the key in the Authorization header instead of the deprecated SIGNATURE query parameter.
  • DNS providers: Mythic Beasts reported a TXT value deleted when the API had removed nothing, as when the zone stores it in another form. It now deletes the value as the API lists it and fails when no record was removed.
  • DNS providers: RcodeZero used the deprecated v1 API with upper-case change types and plain-string records, sent ADD on a name already holding a TXT value, and took a refusal answered with HTTP 200 for success. It now follows the v2 API, writes the whole rrset when adding a second value, and fails on status: failed.
  • DNS providers: Vultr read only the first page of domains and records, so a zone or a TXT value past it was never found. It now follows the meta.links.next cursor through every page.
  • DNS providers: Vercel read the first page of a zone's records only and reported success when a deletion failed. It now reads every page of the documented v5 listing, fails when the API refuses the deletion, and takes an optional team ID for domains owned by a team.
  • DNS providers: Google Cloud DNS took a failed read of the TXT records for an absent record, so a cleanup reported success with the value still published, and it read only the first page of managed zones. A failed read now fails, and every page of zones is read.
  • DNS providers: Porkbun reported a TXT record deleted when the listing or the deletion failed, and sent the TTL as text where the API takes an integer. Both failures now fail the cleanup, and the API's warnings, such as a domain not delegated to Porkbun, are logged.
  • DNS providers: Hurricane Electric reported a TXT record cleared whatever the update endpoint answered, badauth included. A clear the endpoint does not confirm now fails.
  • DNS providers: FreeDNS reported a TXT record created or deleted without checking the outcome, and took an unreadable record list for an absent record. A record missing after creation, still listed after deletion, or a list it cannot read now fails.
  • ACME client, renewals and proxy: a DNS-01 TXT cleanup the provider refused left nothing in the logs. It is now logged as a warning with the provider's answer, as is a failed record listing.
  • Database: a gunicorn worker dropped the inherited connections of the default database engine only. It now drops those of every engine.
  • Scheduler: under gunicorn the scheduled tasks ran in the master, so a worker forked while a task held a database connection shared it with the master. They now run in a dedicated scheduler process that the master starts, restarts within seconds after a crash or a reload, and stops with it, so a long task delays neither requests nor workers.
  • Syslog: the TCP connection opened by the gunicorn master was inherited by its workers, so the frames of several processes could interleave on one stream. Each worker now opens its own connection, leaving the master's open.
  • PKCS#11: a gunicorn worker went on using the library its master had initialized, a state Cryptoki leaves undefined after fork: a module that resets itself in the child, such as SoftHSM with library.reset_on_fork, refused every operation. Each process now initializes the library itself, and keeps the inherited state only when the module refuses a second initialization.

Upgrade notes

  • Algorithms (#397): EST serverkeygen and mTLS no longer give RSA-2048 under an EC, RSA-3072 or ML-DSA CA; ACME orders without a digest, EST, SCEP, WSTEP, Sign CSR, the issue form and CRLs sign with SHA-384 or SHA-512 under a P-384 or P-521 CA, and renewals raise SHA-256 originals there; CRLs already issued are unchanged, only the next one follows the new default. A template whose digest a policy forbids no longer fails issuance or approval with a 400: the signature yields to the policy, with a notice. Under an RSA CA, an EC serverkeygen request now gets P-256, not RSA-2048.
  • XCEP: ECDSA templates are announced as schema 3 with a request hash.
  • Revocation: migration 129 adds the compromised_keys table and fills it from the certificates revoked as compromised and from the revocation records holding such a key's digest. A replacing restore adds the archived keys and never removes one known here.
  • Revocation records written before migration 128 carry no key digest: they learn it when their certificate is deleted, and until then the certificate row itself answers.
  • ACME client: the renewal task now removes the orders left pending for more than 7 days whose order at the CA has expired or whose expiry is unknown, with their TXT records, and audits each removal.
  • DNS providers, ACME CA accounts and webhooks: the upgrade renames duplicate names to name (2), name (3), oldest first, then makes the name unique (migration 124).
  • ACME client: a renewal now keeps the certificate's id and refid, and /renew and the renewal logs no longer announce a new certificate id. Certificates that past renewals imported as new rows stay in the inventory: archive or delete them by hand.
  • Backups: archives now carry ACME client orders, approval requests and key recovery requests, and the duplicate orders migration 121 removes are removed again from the orders a restore writes. An archive written before this version leaves the rows here as they are.
  • Restore: an order renewed or being finalized here since the archive was taken is kept by a merge, with its certificate, pending key and owed revocations. A replacement puts the archived order and certificate back, and its result names the serial of the certificate given up, still valid at the CA, and warns that the restored one may have been revoked there.
  • Restore: an archive written before this version carries those settings as bare numbers. Restored on another installation they are emptied or left out, and must be chosen again; on the installation that wrote it, a number is kept while it still names one of the archived rows.
  • ACME client: orders stopped by the old limit of 5 failed renewals resume at one attempt a day, and orders with no failure count recorded, which were skipped, are renewed again. Turn off the automatic renewal of an order you no longer want renewed, or delete it.
  • Restore: a replacing restore now removes the CRLs, cached OCSP responses, sessions, in-flight ACME server orders, approval requests (from an archive without them), Microsoft CA requests, SAML replay entries and webhook or deployment deliveries (history included) that point at a removed row, detaches ACME client orders, SCEP requests, discovery results and revocation records from it, and cancels the key recovery requests still open on a removed certificate. The restore message and its audit entry list them.
  • Restore: BACKUP_MAX_UPLOAD_MB (default 512) is the restore upload's own ceiling, the decompressed content may be four times that, and the archive may hold one JSON object per 100 bytes of it; a reverse proxy in front of UCM keeps its own body limit. A restore holds the archive in memory, with a peak of several times its size.
  • Templates: the upgrade adds the custom_extensions column, empty for every existing template (migration 127).
  • mTLS: migration 130 makes client certificate enrolments unique per issuer and serial instead of serial alone, first filling a missing issuer from the stored certificate.
  • Scheduler: the scheduled tasks now run in a process of their own (scheduler_main), which adds about 150 MB of memory (proportional set size at rest); the 1 GiB limit of the Helm chart and the 2 GiB of docker-compose.yml leave room for it, and the chart's memory request rises from 256Mi to 512Mi. A reload restarts that process, and its tasks run again from the next pass after the usual 30-second grace, as after a service restart. "Run now" asks that process and waits up to 30 seconds for the result.

๐Ÿ“œ Recent release history (last 2 versions)

[2.249] - 2026-11-05

Security

  • SSH: a requester could set any key ID, which sshd logs as the certificate holder's identity. Each SSH CA now names its certificates principals@CA name and refuses another key ID unless Let requesters set the key ID (allow_custom_key_id) is ticked (#432, reported by @danielrona). A principal can no longer contain a comma, which would read as two in that name.

Fixed

  • ACME client: the ARI identifier of a certificate dropped the leading zero of a serial whose first bit is set (RFC 9773 ยง4.1), so renewal windows and replaces went wrong for such serials. It now follows the RFC, and replaces is dropped on any refusal but a rate limit.
  • ACME client: a first name over 64 characters failed at finalization, once the challenges had passed. The name now goes in the SAN alone, marked critical.
  • ACME client: a renewal waited a fixed 20 seconds for validation and finalization gave up after 30. Both now poll to the CA's configured timeout and honour Retry-After.
  • ACME client: a renewal with key_source=reuse certified again a key revoked as compromised, on its own certificate or an earlier one holding it. It now generates a new key, and an order with an external CSR holding such a key is refused.
  • ACME client: a threshold above two thirds of the certificate's life renewed it far too often. It now renews in the last third, and a renewal no longer asks for an email an existing account does not need.
  • Backups: an HSM provider's PIN or token left in the archive under the source's database key, and read back empty on another installation. The archive now holds it in clear, under its password; a value only another key opens is dropped on restore, with a log entry asking to enter it again.
  • HSM: generating a key under a name OpenBao or Azure Key Vault already held adopted that name, whatever the requested algorithm, and deleting it then destroyed the key of a live CA. The API now answers 409; bring an existing key in with a sync.
  • HSM: two generations at once under one name could both find it free, and a key whose row could not be stored stayed in the HSM. Generations now take turns per provider (409 after 60 s), and such a key is removed unless a row holds its name.
  • Database errors quoted bound values in logs and API messages, an unencrypted private key included. They are now hidden, except what PostgreSQL itself quotes in a DETAIL line: a duplicate key in full, or each column of a refused row up to 64 bytes.
  • ACME client: every successful renewal left a second order on the schedule, so the orders doubled at each cycle. The renewed order now absorbs the one that fetched its certificate, and the upgrade removes the existing leftovers.
  • ACME client: a failed renewal left the order it had opened behind, one row per attempt, and an order the CA had declared invalid still went to finalization, which hid the CA's reason. The order is now removed unless the CA may still complete it, and a scheduled renewal records the reason on the renewed order.
  • ACME client: a status check replaced issued with the CA's valid, which took the order out of automatic renewal. An issued order now stays issued, and the upgrade puts back those still holding a valid certificate that no newer order replaced, proxy orders excepted.
  • ACME client: a finalization that failed on a database error, a refused import or a download the CA refused did not record the error on the order. It now does, without the SQL text and keeping a status the CA reported as valid, so the order can still be renewed.
  • HSM: a CA whose creation failed after its key was generated left that key in the HSM and in the key list. It is now removed, where the provider allows it.
  • HSM: extractable sent as text, such as "false", generated an exportable key. The API now answers 400.
  • XCEP: a CA with no template to announce sent empty policy and OID collections, which Windows clients refuse with WS_E_INVALID_FORMAT. Both are now nil, and the client sees a policy server with no templates.
  • Templates: a switch sent as text, such as "autoenroll_enabled": "false", was stored as on, and a bad is_active failed a whole import. The API now answers 400, and an import skips only that template, with the reason.
  • Restore: text in a boolean column, such as "is_active": "false", turned the setting on. The archive is now refused.
  • ACME profiles: renaming a profile bound to a disabled template was refused as a new binding. A rename now keeps the binding, and a second profile on that template is still refused.
  • Public endpoints: with the HTTP protocol port set to 0 and a Protocol Base URL without a port, CRL, OCSP and AIA URLs were issued as http://host:0/.... They now use port 80, the preflight check probes it, and the upgrade corrects the URLs stored on each CA (discussion #433, reported by @avdpas, thanks @Hemsby).
  • Google Cloud KMS: the notice for ML-DSA keys now says that KMS signs at most 64 KiB, which rules out a larger CRL.

Changed

  • Certificates: serial_number and caref are indexed, so renewal, revocation and protocol lookups no longer scan the table (migration 120).

Upgrade notes

  • ACME client: the upgrade removes each order a past renewal left behind, recognised by holding the certificate and order URL of an older order. It also puts orders back on the renewal schedule if a status check had taken them off, so a certificate you had stopped relying on can renew: delete its order if you do not want that.
  • ACME client: two orders for the same names that have already renewed apart cannot be told from two wanted orders, so both keep renewing: delete the one you do not want.
  • Templates: the API and import accept only true or false for is_active, enforce_san_types, ad_derived_subject and autoenroll_enabled; 0 and 1 are refused.
  • Public endpoints: certificates issued before this update keep a :0 in their CRL, OCSP and AIA URLs: reissue them.
  • SSH: the option is off for every CA, existing ones included. A client that sends key_id gets a 400 until the option is ticked on its CA; sending the CA's own principals@CA name is still accepted.

[2.248] - 2026-11-01

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_dsa scheme, so allowed_signature_schemes and pq_standalone apply 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_algorithm instead 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/certificates with hybrid: {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 one hybrid-bundle ZIP.

  • 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/csrs builds one with related_to, which needs read:private_keys and 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 the pq_standalone rule 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 new ca.unapproved_algorithm check 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 keyAlgo on POST /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_cas and delete:ssh_cas scopes, so write:ssh no longer creates or edits SSH CAs. Upgrade note: roles, groups and API keys holding an SSH scope receive the matching ssh_cas scope 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-challenge name 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/methods answers like the GET, whatever its body.

  • Behind a reverse proxy that sent Connection: upgrade on 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 with Upgrade: websocket is 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 of read:ssh only.

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-cert signed 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.

Full history: CHANGELOG.md


Installation

Docker (Recommended)

# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.251

# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.251

# Run
docker run -d -p 8443:8443 \
  -e SECRET_KEY=$(openssl rand -hex 32) \
  --name ucm neyslim/ultimate-ca-manager:2.251

Debian/Ubuntu

wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.251/ucm_2.251_all.deb
sudo dpkg -i ucm_2.251_all.deb
sudo apt-get install -f

Fedora

wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.251/ucm-2.251-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.251-1.fc43.noarch.rpm

RHEL, AlmaLinux, Rocky Linux 9

wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.251/ucm-el9-2.251-1.el9.noarch.rpm
sudo dnf install ./ucm-el9-2.251-1.el9.noarch.rpm

FIPS 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.251-fips

Ubuntu 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.251_all.deb

RHEL, 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.251-1.el9.noarch.rpm

The 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.251_all.deb

Default Credentials

  • Username: admin
  • Password: changeme123

Change the password immediately after first login!

Documentation

Don't miss a new ultimate-ca-manager release

NewReleases is sending notifications on new releases.