What's Changed
Added
- LDAP: a StartTLS option switches a plain LDAP connection to TLS before the service and user binds, with the existing certificate verification; it was documented but never sent, so passwords crossed in clear (RFC 4513 §3, migration 137).
- SAML: the Single Logout endpoint announced in the SP metadata ignored LogoutRequests. A LogoutRequest signed by the IdP, naming its Destination, issued within the clock window and not seen before, now closes the session of its NameID in that browser and gets a LogoutResponse signed with the SP RSA key, unsigned with a warning when the SP key is not RSA (SAML Profiles §4.4).
- Deployment: a deploy target can be an HTTP API instead of an SSH host, with a URL, a method, encrypted request headers and a body template rendered from the certificate (
{{cert_pem|json}},{{fullchain_pem}},{{cn}},{{serial}}...), through the same delivery queue; the private key is sent only with an explicit opt-in over verified HTTPS, with an optional pasted trust anchor for a self-signed appliance (discussion #428, requested by @gjarassier, migration 131). - EST: the encrypted
serverkeygenkey can now go to an EC client certificate by key agreement (ephemeral-static ECDH, RFC 5753), and the key transport or key agreement and the content cipher follow the CSR's SMIMECapabilities in the client's order of preference, with AES-CBC when no listed content cipher is available. A list naming only key transport or agreement algorithms UCM lacks is refused with the supported ones named, as is an RSA client key under 2048 bits (RFC 7030 section 4.4.2). - EST: with the
allow_empty_usernamesetting ofPATCH /api/v2/est/config, Basic authentication accepts an empty username and the password alone (RFC 7030 §3.2.3). - SCEP: a "Serve on plain HTTP" setting, off by default, also answers SCEP on the HTTP protocol port (8080 by default) for clients such as sscep that cannot use HTTPS; RFC 8894 §7.10 needs no TLS, as the CMS envelope protects the messages.
Changed
- AD Connector: saving a base DN without DC components, which names no domain, is refused with 400; saving or testing a Global Catalog port (3268, 3269) or a server written with a port or an
ldap://scheme is refused too, and a Kerberos realm must match the base DN's in capitals exactly (RFC 4120 §6.1). - SSO providers: an unknown
default_roleis refused instead of being stored as viewer, and an empty one, "None" in the form, gives no implicit role; an existing provider whose default role is empty or unknown keeps viewer (migration 137). - LDAP: a new provider without SSL/TLS uses StartTLS by default, so passwords do not cross in clear (RFC 4513 §6.3.3); existing providers keep their setting.
- ACME server: revokeCert refused an account other than the one that ordered the certificate even when it held valid authorizations for all its identifiers. Such an account may now revoke a certificate this ACME server issued, as RFC 8555 §7.6 requires; an authorization granted by auto-approval does not count.
- ACME server: the
notBeforeandnotAfterof a newOrder were echoed, then ignored, so the certificate got the profile's 90 days. They are now issued exactly: a window longer than the profile allows, outside the issuing CA's, already past or unreadable is refused asmalformed, one a policy forbids asbadCSRat finalize (RFC 8555 §7.4). - WSTEP: a certificate issued under a template carries that template's EKUs instead of the ones the request asks for, as on renewal and in AD CS.
- WSTEP: a request without Key Usage gets the one its template configures, else the one of its template's kind, at issuance and renewal: digitalSignature, keyEncipherment and nonRepudiation for an emailProtection or EFS template, digitalSignature and keyEncipherment for Smartcard Logon, digitalSignature alone for clientAuth, instead of the server pair for all.
- XCEP: the Username/Password policy endpoint announces a client-supplied subject for every template, machine templates included, since that binding carries no directory identity to build one from.
- PostgreSQL: UCM connects with psycopg 3, which sends values apart from the statement, so a database error that quotes the statement no longer carries them. psycopg2 is no longer shipped, and a
postgresql+psycopg2://URL now uses psycopg 3. - Dependencies: SQLAlchemy 2.1 is accepted, now that every PostgreSQL URL reaches psycopg 3.
- EST, SCEP and TSA share one CMS SignedData builder; no wire change.
- EST:
/csrattrslists keyUsage, subjectAltName and extKeyUsage as bare OIDs instead of inside an extensionRequest, a form RFC 9908 §5.2 declares incorrect; thecsrattrs_legacy_encodingsetting ofPATCH /api/v2/est/configrestores the former shape. - EST: enrollment refusals are sent as
text/plainwith the reason, such as a body that is not base64 or not a PKCS#10 CSR, a CA name constraint or the rate limit, instead of an HTML page saying "Enrollment failed" or JSON (RFC 8951 §5). - EST:
simplereenrollwithout a client certificate answers 403 instead of a 401 offering Basic authentication, which that route refuses (RFC 7030 §3.2.3), and/fullcmcanswers 501 instead of 405 (RFC 7030 §3.2.2). - EST: a CSR whose
challengePasswordis not thetls-uniquevalue of its TLS 1.2 connection to UCM, or that arrives over TLS 1.3 or through a proxy where none can be checked, is refused with 400 onsimpleenroll,simplereenrollandserverkeygeninstead of being certified unchecked (RFC 7030 §3.5). - SCEP: a RenewalReq signed by a certificate issued through the same endpoint is authenticated by that certificate, so its challenge password is no longer compared and a rotated secret no longer blocks renewal; a certificate from another profile or protocol still needs the endpoint's challenge.
- SCEP: every FAILURE reply now carries a failInfoText attribute giving the reason (RFC 8894 §3.2.1.4).
- ACME proxy: the directory no longer advertises
keyChange, which the proxy refuses, and a challenge none of the requesting account's orders lists is now answered 404 instead of 403. - ACME proxy: a client's
profile,notBeforeandnotAfterare now passed to the upstream CA, so a CA that does not accept them refuses the order with its own error instead of silently applying its defaults. - SSH: certificates are valid from 5 minutes before issuance, so a server whose clock lags accepts them at once, and the KRL version now grows with each generation instead of staying 0 (PROTOCOL.krl).
- mTLS: the trusted CA is now the only trust anchor of the HTTPS port. A client certificate from its parent or from a sibling intermediate passed the TLS handshake, and with it EST enrollment; it is now refused (RFC 5280 §6.1).
- Dashboard: the Recent Activity feed shows the whole audit trail only with
read:audit, as the Audit Logs page does; without it, a user sees their own events. - TOTP: the account actions that ask for a code (confirm, disable, regenerate recovery codes) accept one time step of clock drift either way, as the login does.
- Access Control: the API no longer lets a custom role inherit from a built-in role, which the page never offered; a role that already does keeps its parent.
- TSA: a request carrying any extension, critical or not, is now refused with unacceptedExtension, and a malformed one, trailing bytes included, with badDataFormat instead of systemFailure (RFC 3161 §2.4.1).
- TSA: tokens carry genTime in whole seconds with a declared accuracy of one second, and
/tsamatchesapplication/timestamp-queryorapplication/octet-streamexactly, ignoring case, instead of any type containingtimestamp-query. - CRL: with delta CRLs enabled, each complete CRL is now followed by a delta on it, so the shared CRL number advances by two per regeneration.
- Approvals: a revocation request approved while its certificate is on hold now revokes it for good with the requested reason, instead of waiting for the hold to be lifted.
- Sign CSR, EST, SCEP, WSTEP and ACME: a subject beyond the RFC 5280 bounds (CN over 64 characters, a country that is not two letters in a PrintableString) was signed as received; it is now refused with the reason, as on the issue form. An EST, SCEP, WSTEP or in-place renewal that keeps its certificate's subject is not affected; an ACME renewal is.
- Sign CSR (single, bulk and approval): a request without Key Usage signed as client, code signing or S/MIME got none, so the key was unrestricted; it now gets its type's Key Usage, as on the issue form.
- Certificates: on the issue form, Sign CSR, EST and WSTEP, a valid emailAddress in the subject is now also an rfc822Name in the SAN for every certificate type, not only email and combined (RFC 5280 §4.1.2.6); SCEP keeps the request's SAN as sent.
- Certificates and ACME: a DNS name holding an underscore (
my_host.example,_sip._tcp.example.com, also as the CN of a server or device certificate without SAN whose template allows DNS names) or in IP-address form is now refused, as RFC 5280 §4.2.1.6 requires. An EST, SCEP, WSTEP or in-place renewal keeps the names its certificate already carries; an ACME order does not. - EST, WSTEP and SCEP: every certificate was recorded as a server certificate, so an S/MIME or client one showed as "Server" in its details and in the API of a renewal approval request. Each now records the type it is signed as (EST: device; SCEP and WSTEP: from its EKUs, by one rule); certificates issued earlier keep their recorded type.
Fixed
- Account: a wrong current password on the password change form answered 401, which logged the user out; it now answers 403 and the form shows the error.
- AD Connector: an LDAP search that the domain controller answered with an error (busy, unavailable, time limit, a base DN it does not hold, missing read rights) read as "account not found", so WSTEP refused the requester with 400. It now counts as a directory failure, answered 500 like an unreachable DC.
- mTLS: a static file request sent without the session cookie, as browsers fetch the favicon, opened a new certificate session and wrote a
login_successaudit event, several per page refresh. Static files no longer open a session, so a page load logs in once. - mTLS: once a session expired, opened by password or by certificate, each of the parallel calls still carrying its cookie logged in again with the client certificate, opening one session and one
login_successevent per call. They now share a single new session, and the web interface redirects to the login page once for a burst of 401 answers. - ACME proxy: every upstream CA error reached the client as a 500
serverInternal, so a rate limit, a refused name or a refused CSR looked like a proxy failure. The upstream type, status, subproblems and Retry-After now reach the client, revocation included (RFC 8555 §6.6, §6.7). - ACME proxy: when the DNS provider refused the TXT record, the challenge was still submitted and failed on the shared upstream account. The order now turns invalid without any upstream request, its other challenges are not submitted either, a retried order on the same authorizations is validated, and the TXT records of invalid or deactivated orders are removed.
- ACME proxy: an upstream error on a certificate download reached the client as a 200 carrying the error body. It is now relayed as an ACME problem with its status.
- ACME proxy: when the upstream CA refused or did not take the challenge response, the order stayed pending with its TXT records published and no later order could validate the name. The order now turns invalid for good with the upstream error, its records are removed, a retried order is validated, and a refused nonce is retried up to three times by the proxy and the ACME client (RFC 8555 §6.5, §7.1.6).
- ACME proxy: a CSR that did not name exactly the order's identifiers reached the client as a 500
serverInternalwhen the upstream CA refused it asunauthorized. It is now refused withbadCSRbefore reaching the upstream CA (RFC 8555 §7.4). - ACME server and proxy: proxy responses other than newNonce and revocation carried no
Link: rel="index", and a successful revocation answered with an emptytext/htmlbody. Every proxy response but the directory now links it, and a revocation answers 200 with no body and no media type (RFC 8555 §7.1, §7.6). - ACME proxy: the directory listed the upstream profiles but
profileandnotAfterwere never passed upstream, so a client got the default profile and validity. Both are now passed, a retried request reuses a pending order only if it carries the requested dates and the samereplaces, and a profile the upstream CA does not offer is refused withinvalidProfile. - ACME proxy: the challenge was read upstream with a POST-as-GET before the TXT record was published, which starts the validation on some CAs. It is now read through its authorization (RFC 8555 §7.5.1, erratum 5729).
- ACME proxy: the upstream Retry-After of an order, authorization or finalization was dropped, and upstream requests carried the HTTP library's User-Agent. Both are now relayed and set (RFC 8555 §6.1, §7.1.3).
- RFC 2136 DNS provider: a server given by host name always failed with an empty message, and a filled-in zone was overridden by the zone the system resolver saw. The name is now resolved and each of its addresses tried, the configured zone is used for the names inside it, and otherwise the zone is read from the configured server's SOA.
- ACME client: with ARI, a certificate renewed at the very start of the suggested window, an expired certificate was still checked and any Retry-After was taken as is. It now renews at a random moment drawn once inside the window, skips expired certificates and keeps the checking interval between one minute and one day (RFC 9773 §4.2, §4.3).
- ACME client: an HTML error page from the CA was reported as a JSON parse error, an RSA account key with an exponent other than 65537 got a padded JWK exponent, and an unquoted
rel=alternatelink was ignored. The status and Retry-After are now reported, the exponent is minimal (RFC 7518 §6.3.1.2) and every RFC 8288 form is read. - DNS providers: Cloudflare, OVH, Bunny, DNS Made Easy, IONOS, NS1, Rackspace, Scaleway, Tencent Cloud, Dynu, Porkbun, Netcup, ClouDNS and INWX followed redirects with their credentials, and a Cloudflare delete the API refused was reported as a success. Redirects are no longer followed and the refusal is reported.
- Approval e-mails: without a Base URL or server FQDN, the link to the request followed the Host header the requester sent; the e-mail now goes without a link.
- Password reset: the e-mailed link pointed at
https://localhost:8443. It now uses the Base URL, else the server FQDN, never the request's Host; when neither is set, Forgot password is hidden and the reset is refused with a warning in the log. - OIDC: an ID token whose
azpnamed another client was accepted when it had a single audience; it is now refused (OpenID Connect Core §3.1.3.7). - LDAP: a second factor the user had enrolled, even one Enforce 2FA required, was never asked at sign-in. LDAP sign-ins now ask for it, as local ones do.
- Certificates: a DNS name with a space, a trailing dot, a wildcard past the first label, a lone
*, an all-digit last label or anxn--label that is no A-label was issued by the form, the API, Sign CSR, EST and SCEP. Such names are now refused with the reason, a 400 or a SCEP failure naming them (RFC 5280 §4.2.1.6), and an internationalized name is issued as its A-label instead of answering 500 (RFC 9549). - Sign CSR, EST and WSTEP: a request without SAN got its CN as a DNS name whatever it held (
DNS:John Doe, and on a sub-CA too), and a non-ASCII CN answered 500. Only a CN in DNS name syntax now gives one, a single label only on a server or device certificate and never on S/MIME or a sub-CA, with a notice when a host-like server CN gives none. - WSTEP: a template without serverAuth signed its certificates as server ones, so a user CN (
jdoe) becameDNS:jdoe. A template with emailProtection or EFS now issues S/MIME certificates, one with clientAuth or Smartcard Logon (VPN Client included) client ones and one with code signing alone code-signing ones, which take no DNS name from a one-label CN. - Sign CSR: a refusal for a parent CA with pathLen 0, a name outside the CA name constraints or an unacceptable key answered 500 "Failed to sign CSR". It now answers 400 with the reason; an empty subject takes as CN the first SAN name that fits in 64 characters.
- CAs: the error for a DNS name constraint with a leading dot suggested
.example.com, the form it refuses. - Rate limiting: a 429 from a login route's own limit carried the general limit in
X-RateLimit-Limit(30 instead of 5) and noRetry-After. It now names the limit that refused the request and when to retry. - ACME server: a body that was not JSON, a payload that was not an object or an unreadable date answered 500
serverInternal. They now answer 400malformed(RFC 8555 §6.2, §7). - ACME server: a missing nonce answered
malformedand a nonce outside base64urlbadNonce, the reverse of RFC 8555 §6.5; HEAD on newNonce answered 204 without a link to the directory. Both errors now carry the right type, and newNonce, on the server and the ACME proxy, answers 200 to HEAD and 204 to GET withLink: rel="index"(§7.2). - ACME server: every response stored a nonce that was never deleted, so the nonce table only grew. The 6-hourly ACME purge now also deletes expired and consumed nonces.
- ACME server: a CSR whose common name is one of the order's IP addresses, as lego v4 and the UCM ACME client send for an IP order, was refused as
badCSR. Such a CN is now matched against the IP identifiers (RFC 8738). - ACME server: the directory advertised the public host as
caaIdentitieseven when CAA identifiers were configured, while finalize checked CAA against the configured ones. Both now use the same list (RFC 8555 §7.1.1). - ACME server: an order past its expiry, or one whose valid authorization had expired, could still be finalized. Finalize now answers 403
orderNotReadyand the order becomes invalid, and an expired valid authorization readsexpired(RFC 8555 §7.1.6, §7.4). - ACME server: an account created with External Account Binding was returned without its
externalAccountBindingfield. The new account object, on the server and the ACME proxy, now carries it with the value of the request (RFC 8555 §7.3.4). - ACME server: a POST-as-GET on a challenge started its validation, so a client reading the challenge before preparing its response, the ACME proxy included, lost the order. An empty payload now only reads the challenge, and only a response object such as
{}starts the validation (RFC 8555 §6.3, §7.5.1). - ACME server: an order accepted with
replacesdid not show it in the order object, and the finalize response lacked the profile. Every order object (newOrder, read, finalize) now carriesreplaces,profileand the requested dates (RFC 9773 §5). - ACME server: tls-alpn-01 accepted a validation certificate whatever its subjectAltName. The SAN must now hold exactly the identifier being validated, one dNSName or one iPAddress (RFC 8737 §3, RFC 8738 §6).
- WSTEP: a request with no
wst:RequestTypeor another one than Issue was served as an issuance, a message whosewsu:Timestamphad expired was accepted, and the fault for an unreadable request did not relate to its message. Such requests now get a SOAP fault (wsse:MessageExpiredpast a 5 minute skew), which carriesRelatesTo. - WSTEP: a PKCS#10 request sent under the
#PKCS7ValueType, as in the MS-WSTEP example, was refused as a malformed CMS envelope. The request format is now read from its content, whatever the ValueType says. - WSTEP: a plain PKCS#10 request got no CMC Full PKI Response in its answer, which MS-WSTEP requires for every request, and the one a CMC request got carried its status under a wrong OID. Every request now gets one, with CMCStatusInfoV2 under
id-cmc-statusInfoV2(RFC 5272 §6.1.1). - WSTEP: the template a request named (szOID_CERTIFICATE_TEMPLATE by OID, szOID_ENROLL_CERTTYPE by name) was ignored and the template chosen by EKU, so of two templates with the same EKUs the oldest always won, and a template the CA does not offer was issued anyway. The named template is now issued, an unknown, inactive or conflicting one is refused, and a request naming none is still matched by its EKUs.
- XCEP: the policy announced no certificate template extension, so Windows CertEnroll named no template in its request and of two templates with the same EKUs the oldest was issued. Each template now announces its
szOID_CERTIFICATE_TEMPLATEextension, which Windows copies into the request, and its revision is the one the issued certificate carries. - WSTEP and XCEP: a fault caused by the request (unknown template, EKUs the template does not issue, failed authentication, malformed message) carried the
s:Receivercode, and a failure on the CA side (signing error, unreachable directory, no template configured to issue under) went out as HTTP 400. Faults now follow SOAP 1.2:s:Senderwith HTTP 400 for the request's (a Kerberos requester missing from Active Directory included),s:Receiverwith HTTP 500 for the server's. - WSTEP: a request without Extended Key Usage, such as
openssl req -newwith a CN alone, was issued under the built-in OCSP Signing template, and one asking for OCSP Signing got serverAuth instead. A template whose EKUs are all never issued to an enrollee is now left out of the XCEP policy, and a request or renewal under it is refused. - WSTEP: an AD Connector enabled without a server or base DN answered a Kerberos request with HTTP 400 "The requester was not found in the directory". It now answers
s:Receiverwith HTTP 500, and an account whose objectSid is missing or malformed gets its own reason. - WSTEP: with XCEP and WSTEP set to two different CAs, the certificate carried a template OID derived from the WSTEP CA rather than the one XCEP announced, so Windows autoenrollment did not recognize it and enrolled again at every pulse. The certificate now carries the OID XCEP announced and still renews under its template once the XCEP CA setting changes; an OID derived by another UCM instance names no template.
- Access Control: the built-in roles showed the permissions seeded at installation (12 for Operator, which is granted 41) and Administrator's
*as ignored. They now show what UCM applies to them. - ACME DNS-01 on DuckDNS, Hurricane Electric and FreeDNS: these providers hold one TXT value per name, so the cleanup of one order cleared the value another order was still validating. The name is now left alone while another order in progress uses it.
- ACME DNS-01 on DuckDNS, Hurricane Electric and FreeDNS: an order for a domain and its wildcard could not hold both values and failed at validation, and the ACME proxy replaced the CA's value with its clients' own. Such an order, or on DuckDNS one naming two hosts under the same subdomain, is now refused at the request before the CA is asked, and the proxy publishes only the CA's value there.
- ACME client: a CA refusal of
replaceswhose message merely contained "rateLimited" was taken for a rate limit and not retried. A rate limit is now recognised by its problem type or a 429 only (RFC 8555 §6.7). - WSTEP: a certificate issued or renewed under a template did not record it, so the template's usage count left it out, the template could be deleted while in use, and its policies did not apply. The matched template is now recorded at issuance, so its approval policies cover revocation and renewal and an in-place renewal applies its SAN types, policy OIDs and custom extensions; certificates issued before are left as they are.
- EST: the encrypted
serverkeygenkey came in a SignedData of typedata, signed by the generated key itself and without signed attributes. It now comes in an RFC 5958 key package (id-ct-KP-aKeyPackage) with content-type and message-digest attributes, signed by anEST key generatorcertificate that the issuing CA issues on first use, HSM CAs included (RFC 7030 section 4.4.2, RFC 5652 section 5.3). - EST: without a requested encryption key,
serverkeygenreturned a PKCS#8 encrypted with the Basic password, or a CMS over mTLS. The key now comes as unencryptedapplication/pkcs8under TLS, and a request naming a key UCM does not hold, or a client certificate that does not allow key transport, is refused (RFC 7030 section 4.4.1). - Uploads were spooled to the system temp directory, so a restore larger than a small
/tmptmpfs (100 MB in the production compose file) failed while the form was read, and a memory-backed/tmpheld the archive in RAM. Uploads are now spooled underDATA_DIR/tmp, falling back to the system temp directory only when that is not writable. - EST
simplereenrolland WSTEP renewal: a CSR without SubjectAltName got the renewed certificate a name built from the CN, so the device lost the names it served. The renewed certificate now keeps the SubjectAltName of the one being renewed, or none when it had none (RFC 7030 §4.2.2). - EST: a CA label named after an operation (
simpleenroll,cacerts...) was accepted and served. Such a label is now refused when saved, and ignored and left out ofGET /api/v2/est/configwhen already stored (RFC 7030 §3.2.2). - SCEP: a RenewalReq signed by a certificate the CA issued was refused unless it carried the current challenge password, so a client that omitted it, as RFC 8894 §2.4 recommends, or sent the original one after a rotation could not renew. A certificate issued through the same endpoint now authenticates the renewal without the challenge.
- SCEP: a challenge password sent in the CSR as a BMPString or UniversalString was read as UTF-8 and refused. Every DirectoryString type is now decoded by its type (RFC 2985 §5.4.1).
- SCEP: GetNextCACert served as the rollover certificate any CA sharing the current CA's display name, even one with another subject and key, or a revoked or offline one. It now serves only a CA with the same parent and subject DN that SCEP can use (software RSA key) and expires later, and answers 404 otherwise.
- In-app help: the SCEP URL was given as
/scep, which serves the web interface, and HTTPS was said to protect the challenge password. The help now gives/scep/pkiclient.exeand the profile URLs, and says the CMS envelope protects the challenge. - SCEP: a request enveloped with single DES or an unknown cipher or key transport failed as badMessageCheck, and no failure said why. It now fails as badAlg, and every failure carries its reason in failInfoText, the administrator's rejection reason included (RFC 8894 §3.2.1.4, §3.3.2.2).
- SCEP: a request refused by a policy or for its subject or names got that answer before its challenge password, Intune validation or renewal signer was checked, so a wrong password got badRequest instead of badMessageCheck and Intune never heard of the refusal. These checks now come after authentication, and Intune is notified of the failure.
- Certificates: an S/MIME certificate whose CN is dotted like a person's name (
john.doe) was checked as a DNS name against the CA's name constraints, so a CA limited to its domain refused it on the issue form, Sign CSR, WSTEP, SCEP and approval. Its CN is no longer checked as a DNS name unless the certificate also carries serverAuth. - ACME client: an order or preflight for a name with an all-digit last label (
host.123) or anxn--label that is no A-label went to the CA, which refuses it. Such names are now refused upfront, by the rule issued certificates follow (RFC 5280 §4.2.1.6). - SCEP: a POST without
operationwas answered with the capabilities. It is now processed as the PKIOperation it must be (RFC 8894 §4.3). - SSH: deleting a revoked certificate removed it from the CA's KRL, so servers checking the KRL accepted it again. A revoked certificate now stays, and its deletion is refused with 409, until it expires.
- SSH: any critical option was signed, on host certificates too, so a typo or unknown option produced a certificate sshd rejects. Only force-command, source-address (checked as addresses or CIDR networks), verify-required and
name@domainoptions are signed, on user certificates only (PROTOCOL.certkeys). - TSA: a CA certificate expired or not yet valid still signed timestamps no verifier accepts, and the dashboard showed the TSA online.
/tsanow answers 503 and the dashboard shows it offline. - TSA: the token's messageImprint was rebuilt with NULL hash parameters, so a request sent without them got another imprint back. It is now copied from the request as sent (RFC 3161 §2.4.2).
- TSA: rejected requests were never counted, so the failed statistic stayed at 0, and the page header showed no total. Rejections are now audited as
tsa.timestamp_rejectedand counted, and the header shows the total. - TSA: the README and in-app help promised
openssl ts -verifyand jarsigner with the default CA-certificate signer, which both refuse. The help now says these verifiers need a dedicated signer, and the TSA page warns while the CA certificate signs or the policy OID is still the1.2.3.4.1example. - CRL: two generations running at once, such as a revocation and the scheduler, could sign two different CRLs under the same number, and installations not upgraded through migration 004 lacked the unique index. The number is now allocated under a lock taken before the revocations are read, so a higher number always lists what a lower one did, and the index exists everywhere; migration 134 removes the older duplicates (RFC 5280 §5.2.3).
- CRL: a CA restored on another server, or deleted and imported again, restarted its CRL numbers at 1 below those already published. The highest number of each CA name and key is now kept apart from the CA, carried by backups and never lowered by a restore, and an uploaded external CRL must carry a number above it.
- CRL: after each revocation the delta CRL served still referred to the previous complete CRL until the next scheduled delta, hours later. A delta on the new base is now generated right after every complete CRL.
- CRL: a revocation dated before the complete CRL but recorded after it was on neither that CRL nor its deltas. A delta now lists what its base lacks or lists under another reason (RFC 5280 §5.2.4).
- Certificates: a certificate on hold could not be revoked for good, only released and then revoked, which made it valid in between. It can now be revoked with another reason from the Revoke button, the bulk action, ACME or an approved revocation request, its related certificate with it, the invalidity date defaulting to the start of the hold (RFC 5280 §5.2.4).
- OCSP: the statistics gave the number of cached responses as both the requests and the cache hits, so the hit rate always read 100 %. They now count the requests answered since the service started and those served from the cache.
- OCSP: an answer to a request with a nonce was sent with
Cache-Control: no-cache, no-store, and the others with a fixed one-hourExpiresthat could outlive nextUpdate. Answers now follow RFC 5019 §6.2: nono-cacheorno-store,Expiresat nextUpdate,max-agenever past it,Last-ModifiedandETag. - OCSP: a POST whose Content-Type was
application/ocsp-requestin another case was answered malformedRequest. Media types are now compared without case (RFC 9110 §8.3.1). - ACME proxy: an order whose authorization the client deactivated was served valid, with its certificate, once another account's order completed the shared upstream order. It now stays invalid without a certificate, and that certificate is served only to an order that may issue (RFC 8555 §7.5.2).
- ACME server and proxy: a URL or method no ACME route serves, a GET on a POST-only resource included, got a generic 404 without the directory link. It now gets a
malformedproblem document linking the directory, as 405 withAllowwhen another method is served (RFC 8555 §6.3, §7.1). - ACME proxy: a TXT record whose deferred deletion a restart interrupted stayed published for good when no client read its failed order again. The 15-minute order housekeeping task now deletes it, unless another pending order still needs the value.
- ACME proxy: renewalInfo was computed locally, so a revocation or an incident at the upstream CA never brought renewals forward. The window of the upstream CA is now relayed with its Retry-After when its directory announces renewalInfo, the local one served otherwise (RFC 9773).
- ACME server: renewalInfo read the revocation of one stored row of a certificate, so a revoked imported copy still got the normal window. A revocation recorded on any of its rows now gives a window in the past (RFC 9773).
Security
- Database: on a PostgreSQL server whose messages are not in English, an error still carried the refused row of a NOT NULL or CHECK violation, or the refused input of a data error, private keys included. The message is now rebuilt from the error's fields whatever the server language, and a data error keeps only its SQLSTATE.
- SSO: with role mapping rules set, a user no rule matched still signed in with the default role, viewer unless changed. Such a user is now refused at every SSO sign-in, with an audit entry; without rules the default role still applies.
- SSO: an OIDC, SAML or LDAP identity whose username, or e-mail local part, matched an existing account signed into it, the local administrator included, and rebound that account to itself. A name or e-mail now matches only an account of the same provider not yet bound to an identity, never by an e-mail the OIDC IdP marks unverified; a local account or one bound to another identity is refused at sign-in, with an audit entry (OpenID Connect Core §5.7).
- SSO: an identity that older releases bound to a local account of the same name kept that binding through Link to SSO, so it opened the account and the real user was refused. Link to SSO and migration 137 now drop it, and the next sign-in binds the account.
- Remote syslog: only seven resource types were forwarded, so audit purges, API keys, roles, settings, policies and SSO login failures never reached the collector, and the ACME box matched almost no ACME event. Every audit event is now forwarded unless its category is unchecked, ACME covers every ACME event, and entries written inside a transaction, the audit purge included, leave once committed.
- Remote syslog: narrowing the forwarded categories or turning forwarding off was recorded under System once the change applied, so the collector never heard of it, and unchecking Users also muted role, password and MFA changes. The change now reaches both the previous and the new collector, and account changes and the System events that touch keys, encryption, restores, the database or update settings are always forwarded.
- Audit log on PostgreSQL: an entry whose User-Agent, resource id list, resource name or username was longer than its column, or held a NUL character, was dropped while the action went through, so a client could hide its own actions behind a long User-Agent. These columns are now TEXT, every field is cut to a bounded length and its NUL characters escaped before it is sealed, and an entry that still cannot be written is logged as an error and sent to syslog.
- Certificate policies: a DNS pattern written
*.company.com, as the form suggests, covered only that literal name, so approval and the policy's rules skippedwww.company.com, whilecompany.comalso caughtevilcompany.com. Patterns now match by DNS label, ignoring case and a final dot:*.company.comcovers the names under company.com,company.comthe domain as well (RFC 4343). - WSTEP: on the Kerberos binding, a machine template, a "Build subject from Active Directory" template or a request matching no template kept the subject and SAN the request named, and a template keeping them issued the EKUs the request asked for, so any domain account could obtain a server certificate for any name. The subject and SAN now come from Active Directory unless the matched template lets the client supply them, that template issues only its own EKUs, and a machine template refuses user accounts and an AD-derived template refuses computers.
- WSTEP: on the Kerberos binding with the AD Connector enabled, a principal of another realm, one naming no account, or a base DN naming no realm, such as one written with spaces (
DC=corp, DC=example), skipped the SID lookup, so the certificate was issued without the SID extension (KB5014754). Such a request is now refused, 400 for the principal and 500 for the connector setting, and a spaced base DN is read as written. - AD Connector: an account name found twice, as on a Global Catalog that also searches child domains, or an Enroll ACL group name shared by several groups, resolved to the first entry, so a computer account named after a child-domain controller got that controller's SID and DNS name. Both are now refused with 500.
- TOTP: a code already accepted was accepted again within its window, at a second login or to disable 2FA. Each code is now accepted once and an older one after it is refused (RFC 6238 §5.2, migration 135).
- Sessions: a login kept the session identifier presented before it, so an identifier planted in a browser became the victim's session. Every login, SSO and LDAP included, now issues a new identifier and drops the old one (RFC 6265 §8.4).
- Sessions: changing or resetting a password, an admin 2FA reset and "revoke all sessions" left the account's other sessions signed in. They now end them; the session the user acted from stays, under a new identifier.
- API keys: a key could create a key that never expires, and regenerating a revoked or expired key brought it back for a year. A key created or regenerated with an API key now expires no later than that key, regenerating keeps the original lifetime, and a dead key answers 409.
- Access log: a password reset token and an OAuth
codeorstatewere written in clear with the request line and the Referer. Their values are now replaced by[REDACTED]. - Login: a locked account answered 429 before its password was checked, an unknown name 401, so the status told which accounts exist. A locked account now gets the same 401 "Invalid credentials" at the password and LDAP doors, and the refusal is audited.
- Dashboard: the Recent Activity feed returned the latest audit entries, other accounts' actions and names typed at a failed login included, to any signed-in account.
- mTLS behind a proxy: a certificate presented by its serial number alone, without
X-SSL-Client-I-DNorX-SSL-Client-Cert, opened the enrolment bearing that serial, even one of another CA. Such a serial now opens only an enrolment that recorded no issuer, and enrolling a presented certificate requires its issuer (RFC 5280 §4.1.2.2). - EST: a client certificate that another CA of this server had revoked, even as compromised, still authenticated
/simpleenrolland/serverkeygenand obtained certificates for any name. It is now refused once it, or a CA above it, is revoked, or once it has expired, whichever CA of the server issued it, and/api/v2/mtls/enrollno longer enrols such a certificate (RFC 7030 §3.3.2, RFC 5280 §6.1.3). - EST: a CSR whose key was revoked as compromised was certified again. It is now refused with 400, as on the ACME client.
- mTLS: a certificate issued under a sub-CA whose parent was revoked still opened a session and could be enrolled through
/api/v2/mtls/enroll-import. Both are now refused, as on/api/v2/mtls/enroll(RFC 5280 §6.1.3). - Private keys: no installation method created a master key, so CA, certificate and SSH CA private keys and stored secrets sat unencrypted in the database until encryption was enabled by hand. UCM now creates
/etc/ucm/master.key(0600) at its first start and encrypts what is already stored, and the Dashboard warns while no key is loaded. - Private keys: in a container whose
/etc/ucmis not a volume of its own (anonymous, tmpfs or emptyDir), Settings › Security now refuses to enable encryption (409) rather than write a key the next recreation would lose. Once a master key is created at start, the plaintext key copies underdata/privateare removed.
Upgrade notes
- SSO: a provider with role mapping rules now refuses the users none of them matches; add a rule for a group every allowed user is in, or remove the rules to fall back to the default role.
- SSO: a local account is no longer merged into an SSO identity of the same name at sign-in; an administrator links it with Link to SSO first. An account also belongs to one provider: a user who signed in through two providers under the same name (LDAP and OIDC, for example) is refused on the second one.
- SSO: an identity whose stable identifier changes is refused instead of being linked again, for example an LDAP entry that moves in a directory without entryUUID or objectGUID, a change of the LDAP unique ID attribute, or a SAML NameID in emailAddress or unspecified format that changes; use Unlink then Link to SSO on the account.
- SSO: an SSO account that a same-named identity of another provider signed into before this release stays bound to that identity, since nothing records the original one; look for sign-ins that rebound a provider in the audit log and use Unlink SSO then Link to SSO on each such account. Local accounts are unbound by migration 137.
- SSO: the users of a provider deleted before this release are not taken back by a new provider; use Unlink then Link to SSO on each account. After this release, a provider created again for the same IdP takes them back.
- Remote syslog: the collector now receives every audit event type, so expect more volume; unchecking a category still stops its own events. Migration 133 turns four
audit_logscolumns into TEXT on PostgreSQL without rewriting the table. - Remote syslog: the Users box now mutes sign-ins and logouts only, the System box no longer mutes key, encryption, restore, database and update settings changes, and
syslog_config_updatedis recorded with resource typesettingsand the previous configuration in its details; leaving every box unchecked is saved as every box. - Certificate policies: a
*.xDNS pattern now applies to the names under x, so requests issued directly until now may wait for approval or meet the policy's rules; anxpattern no longer applies to names such asevilxorx.other.org. A pattern holding a comma or a space, which matched nothing, is now refused when the policy is saved: create one policy per domain. - WSTEP Kerberos binding: a request naming its own subject under a machine or "Build subject from Active Directory" template, or under no template, now gets the subject from Active Directory, and is refused when the AD Connector cannot resolve the requester. A script that chooses the subject must use a template whose subject the client supplies, which must configure its EKUs, or the Username/Password binding. A user account requesting a Web Server, VPN or OCSP Signing template over Kerberos (certreq from a user session) is now refused, and must use such a template or that binding.
- WSTEP: a certificate 2.251 issued to a request without Extended Key Usage carries the OCSP Signing and serverAuth EKUs of the built-in OCSP Signing template; its renewal is now refused and autoenrollment leaves it in place, so enroll such clients again under another template rather than renewing it.
- Sessions: migration 135 adds
users.totp_last_stepandusers.session_version; sessions open at the upgrade stay valid. A script that signs in with a TOTP code must not reuse a code within its 30 s step. - Private keys: an installation that never enabled encryption creates
/etc/ucm/master.keyat its next start (in a container, only when/etc/ucmis a named volume or a host directory) and removes the plaintext copies underdata/private, so back that file up and give any service that reads a copy a deployment target; a container without such a volume gets no key, and enabling encryption there is refused until/etc/ucmis mounted;UCM_AUTO_MASTER_KEY=false, or disabling encryption in Settings › Security, keeps the previous behaviour. Every start with a key now encrypts the values still stored unencrypted, and a key that does not decrypt the stored ones enters safe mode. - mTLS behind a proxy: a proxy that forwards the serial number without
X-SSL-Client-I-DN(nginx$ssl_client_i_dn, ApacheSSL_CLIENT_I_DN) orX-SSL-Client-Certmust now forward one of them, or certificate login fails with a warning in the log; the I-DN is read in RFC 4514, RFC 1779, OpenSSL one-line and legacy forms. - EST: a client that reads the encrypted
serverkeygenanswer must take the key from the RFC 5958 package and verify it with theEST key generatorcertificate the SignedData carries, which chains to the issuing CA from/cacertsand is listed, and revoked when needed, with the CA's certificates like a delegated OCSP responder; the next encrypted request then gets a new one. Migration 132 adds theest_signer_certstable that names the current one; backups carry the certificate but not that table, so a restored installation issues a new one on first use. - mTLS: a client whose certificate comes from another CA of the trusted CA's hierarchy, an EST client re-enrolling with a certificate from a sibling CA included, is now refused at the TLS handshake; choose their common parent as trusted CA to keep accepting it.
- EST:
serverkeygenwithoutAsymmetricDecryptKeyIdentifiernow returns the key unencrypted, without the Basic password, and over mTLS the key type is chosen as over Basic, ML-DSA under an ML-DSA CA included. - PostgreSQL: on DEB and RPM the upgrade's pip install pulls psycopg 3, compiled against the system libpq under
UCM_FIPS_BUILD=1with the build tools it already requires; Docker images need nothing. Apostgresql+psycopg2://URL keeps working unchanged. psycopg 3 prepares a statement after five executions on a connection, so behind a transaction-mode pooler without prepared statements (older PgBouncer, some managed poolers) setUCM_PG_PREPARE_THRESHOLD=0. - EST: a FortiGate or other client that stops enrolling after the
/csrattrschange gets the former answer back with{"csrattrs_legacy_encoding": true}onPATCH /api/v2/est/config. - EST and mTLS: a device or user that authenticates with a certificate revoked or expired on this server, or issued under a revoked CA, must be given a valid one.
- EST: a client that puts a password in the CSR
challengePasswordkeeps enrolling with{"ignore_challenge_password": true}. - EST: a CA label named after an operation no longer shows on the EST page and is dropped on its next save; give its clients a label under another name.
- Certificates and ACME: rename the hosts whose DNS name holds an underscore and shorten any CN over 64 characters before their next renewal, since ACME orders (cert-manager, certbot, lego), initial EST and WSTEP enrollments, Sign CSR requests and server certificates from the issue form carrying them are now refused.
- SCEP and Intune: a profile whose SAN DNS is built from a device name holding spaces, apostrophes or underscores (such as
{{DeviceName}}on iOS) must use a DNS-safe value before the upgrade, since each enrollment carrying it, Intune renewals included, is now refused with the reason. - WSTEP: with the AD Connector enabled, users and computers of a trusted realm other than the connector's domain are now refused on the Kerberos binding instead of getting a certificate without SID; they can use the Username/Password binding, or a UCM whose connector points at their own domain.
- AD Connector: name an Enroll ACL group whose cn other groups share by its DN, and move a connector saved on a Global Catalog port (3268, 3269), or with a port in a server name, to 389 or 636 of a domain controller.
📜 Recent release history (last 2 versions)
[2.251] - 2026-11-06
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(asearch_pathin 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
serverkeygenand user mTLS certificates. The key now takes the issuing CA's strength, in the family of theserverkeygenrequest'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_renewalthen revoked. The certificate is now renewed in place, keeping its id, andcertificate.renewedis 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_failedwebhook 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
processingfor good. Such an order now goes back topendingwith 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-challengerecord 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, emittingacme_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
wwwcopies 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
/dnszonesendpoints, deletes one value throughrecords/deleteand 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/zonesendpoints, 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
Authorizationheader instead of the deprecatedSIGNATUREquery 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
ADDon 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 onstatus: 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.nextcursor 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,
badauthincluded. 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
serverkeygenand 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 ECserverkeygenrequest 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_keystable 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
pendingfor 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
/renewand 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_extensionscolumn, 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 ofdocker-compose.ymlleave 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.
[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 nameand 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
replaceswent wrong for such serials. It now follows the RFC, andreplacesis 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=reusecertified 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
issuedwith the CA'svalid, 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:
extractablesent 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 badis_activefailed 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_numberandcarefare 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_subjectandautoenroll_enabled; 0 and 1 are refused. - Public endpoints: certificates issued before this update keep a
:0in their CRL, OCSP and AIA URLs: reissue them. - SSH: the option is off for every CA, existing ones included. A client that sends
key_idgets a 400 until the option is ticked on its CA; sending the CA's ownprincipals@CA nameis still accepted.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.252
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.252
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.252Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.252/ucm_2.252_all.deb
sudo dpkg -i ucm_2.252_all.deb
sudo apt-get install -fFedora
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.252/ucm-2.252-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.252-1.fc43.noarch.rpmRHEL, AlmaLinux, Rocky Linux 9
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.252/ucm-el9-2.252-1.el9.noarch.rpm
sudo dnf install ./ucm-el9-2.252-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.252-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.252_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.252-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.252_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation