What's Changed
Added
- The Active Directory Connector takes several domain controllers, one per row, and lookups fail over between them, so one DC being down no longer stops certificate enrollment. Naming each DC by its own hostname is also what makes
Verify SSL Certificateusable on a multi-DC domain, where a domain controller certificate carries only its own host in the SAN (#363, by @stefanelul2000). - A scheduled health check binds to each of the connector's domain controllers on a configurable interval, so an unreachable one is skipped at enrollment time instead of being rediscovered by a request that waits out its connect timeout. The verdict shows on the settings row and against each DC in the dialog; it expires, and it can never rule out every DC (#363, by @stefanelul2000).
- Intune app registrations are defined once, under SCEP › Intune app registrations, and picked per SCEP profile: one tenant, client ID and secret shared by the profiles that validate with it, tested and rotated in one place. (#358)
- CRLs can be deployed over SSH/SFTP whenever they change, with durable per-target delivery, PEM parent-CRL bundles or a single DER CRL, on-demand deployment, and editable certificate and CRL bindings. Reload commands now belong to each binding so one SSH target can safely serve different services. Pending retries show their last error and a live countdown to the next attempt, refresh automatically while waiting, and bindings can be enabled or disabled from their edit dialog.
Changed
- The Active Directory Connector requires a bind DN and a bind password while it is enabled, in the settings form as well as in the API, and will not bind without both anywhere it binds: lookups, the scheduled health probe and either connection test. On upgrade, an enabled connector saved without a password stops answering lookups until credentials are entered in the settings dialog, and an API client must send both when it enables one (#363, by @stefanelul2000).
- A SCEP profile names its Intune app registration instead of carrying the credentials; migration 092 gives every existing Intune profile a registration named after it, shared by the profiles that used the same tenant, client ID and secret. A different secret for the same tenant keeps a registration of its own, reported in the log for a merge by hand; two registrations for one tenant and client ID cannot be created through the API. The pre-092 profile fields stay accepted for one release.
- Deployment target settings now contain only the reusable SSH/SFTP connection. Destination paths, file options and reload commands are configured on each certificate or CRL binding; migration 091 copies existing target reload commands to existing bindings.
Fixed
- The Certificates table and the certificate pickers named a certificate by its description, which hid its Common Name. The Common Name now comes first with the description alongside, in the list, its CSV export, the certificate pickers and the bound HTTPS certificate (#365, by @B0F1B0).
- The TLS certificate selector of the public ACME endpoint offered only the first 20 certificates, and the certificate picker never showed how many SANs a certificate carries. The selector now lists every certificate, and the picker counts the SANs of a certificate that has several.
- In the Docker image, SoftHSM kept its tokens outside the data volume unless a separate volume was mounted, so recreating the container lost every SoftHSM key. Tokens now live in the data volume, and those of a volume at
/var/lib/softhsm/tokensare carried over at the first start (seedocs/HSM_DOCKER.md). - Saving an HSM provider without retyping its PIN, password, client secret or token replaced the stored secret with an empty one. The stored secret is now kept unless a new one is typed.
- On DEB and RPM installs, the service could not write to the SoftHSM token directory, so creating a SoftHSM key failed with a read-only file system error. The unit now opens that directory to the service.
- A CSR, certificate or CRL signed by an EC key held in a PKCS#11 or Azure Key Vault HSM carried the raw
r || ssignature the HSM returns, so OpenSSL and every external signer rejected it. The signature is now DER-encoded as X.509 requires on every path, the generic key signing endpoint included, and a CA created or renewed on an HSM key keeps the digest matching its curve (#366). - The templates list called a template Certificate or CA from the letters "ca" in its name, so the seeded Email Certificate (S/MIME), Client Authentication and Smartcard Logon were each shown as a certificate authority. The column reads the
is_systemflag the API already returns and says System or Custom (#364, by @stefanelul2000). - The detail pane of a system template offered an Edit and a Delete the server answers 403 to. Both are greyed with the reason now, Duplicate and Export stay available, and a Show system toggle hides the built-in templates, remembered across visits and forced on while no custom template exists (#364, by @stefanelul2000).
📜 Recent release history (last 2 versions)
[2.232] - 2026-09-21
Added
- The server's own application log can be read from the interface, under System Logs: the source (application log, gunicorn access and error streams, the systemd journal where it exists), a subsystem, a level floor, a time window, a search and an exclusion, all applied on the server, with secrets redacted before anything leaves the process and a traceback kept as one entry. Reading is restricted to administrators and deliberately not audited, since the audit trail is written to that same log and the page polls (#361, by @stefanelul2000).
- A guide to enrolling devices through Microsoft Intune's SCEP challenge validation, from the Entra app registration to the per-platform Intune profile, in
docs/integrations/intune-scep.md(#360, by @stefanelul2000).
Changed
- The CLA check counts a commit whose email is linked to no GitHub account as the pull request author's, who ticks the attestation line the new pull request template carries instead of signing under the commit's name.
- The service runs with the
systemd-journalsupplementary group, granted in the unit file on DEB and RPM, so the diagnostic bundle and the log viewer can read the unit's journal (#361, by @stefanelul2000). - The Attach target dialog names the certificate it is attaching, so two certificates opened one after the other no longer look alike (#359, by @B0F1B0).
- Attaching a deployment target now offers the Include Root CA box of the export dialog for the full chain file. New bindings ship the leaf and the intermediates only, which is what a TLS server should send; bindings created before this release keep the root as they always did, and the binding row says so (#357, by @B0F1B0).
Fixed
- Redacting a private key from the logs could take tens of seconds on a log seeded with unclosed
BEGINmarkers, which anyone could plant through the SCEP User-Agent. The block is now found in one pass and the User-Agent is logged bounded. - Restoring a backup written before 2.232 gave every deployment binding the new default and dropped the root from its full chain. A column the archive does not name takes the value its version implied, so those bindings keep the root as they did.
- In the log viewer, an end bound given to the minute excluded that minute but for its first second, and a copied line of the access, error or journal source was rewritten in the application log's shape. The end bound now closes its minute or day, and a copy is the line as the log wrote it.
- In the log viewer, a slow answer could land over the filters chosen since it was asked, and ticked rows kept their tick across a change of data. Only the newest request fills the page now, and the tick is cleared with the data.
- The CLA check failed on any pull request without a co-author trailer, accepted a signature line added by someone other than its signatory, and let an existing line be reworded; the v1.1 agreement also called itself v1.0 and pointed at the v1 signature file. All four corrected before the check runs for the first time.
- The application log was written to a file nothing could read back: a container logged to standard output only, and a native install whose
/var/log/ucmcould not be opened fell back to stderr, so the diagnostic bundle shipped without it. One file is now chosen at startup,/var/log/ucm/ucm.logwhere it opens andDATA_DIR/ucm.logotherwise, withUCM_LOG_MAX_BYTESandUCM_LOG_BACKUPSfor its rotation (#361, by @stefanelul2000). - Most of UCM's own secrets were not redacted from the diagnostic bundle: the pattern wanted a word boundary, which an underscore does not give, so
client_secret=,challenge_password=and their kind went out in full, as did JSON passwords, cookies and the password in a URL. The names UCM's settings and provider credentials actually use are covered now, in query-string, JSON and quoted forms, and the log viewer shares the pass (#361, by @stefanelul2000). - A Windows device enrolling through Intune was refused at the certificate build once its request decrypted: the NDES client writes a BasicConstraints of CA:FALSE with a path length, a shape the parser rejects as a whole. The extensions of such a request are now read with that constraint taken as a plain end-entity one, which UCM never copied anyway; the same reading serves WSTEP and renewals (#228, reported by @stefanelul2000).
- Deleting a CA that had served SCEP enrollments failed on PostgreSQL with a foreign-key error after its files were already removed, and left orphaned rows on SQLite. The enrollment history now goes with the authority, a CA still named by a SCEP profile, an ACME domain or an issuance policy is refused with 409 like one with certificates, and the files are removed only once the deletion is committed.
- An iPhone enrolling over SCEP refused every certificate from a CA whose own certificate is shorter than the leaf: the reply carried the CA certificate next to the issued one, DER ordering put the CA first, and Apple's client pairs the first certificate with its key. The reply now carries the issued certificate only, which is all RFC 8894 requires (#228, reported by @stefanelul2000).
- A certificate issued over SCEP through a profile's template was not counted against that template (#228).
- The SCEP endpoint kept advertising its capabilities with SCEP switched off, and a switched-off or unconfigured endpoint answered 500 as if the server had crashed. Every operation now honours the switch, and a refusal comes back as 503 with its reason, or 404 for an unknown profile.
- A Windows device enrolling through Intune was refused at the very last step with
badMessageCheck: the Windows SCEP client wraps the session key with RSAES-OAEP, and UCM only unwrapped PKCS#1 v1.5. Both are accepted now, and the reply is wrapped the way the request was (#228, reported by @stefanelul2000). - A SCEP request reusing an already-issued transactionID with a different key was answered with the certificate of the first key, which the device cannot pair with its own. It is refused with
badRequestnow, and the log names the transaction (#228). - The Create CA wizard pre-ticked the serverAuth Extended Key Usage on every intermediate CA, so a CA created from the interface still restricted every chain beneath it to TLS servers and FreeRADIUS rejected the client certificates issued under it; v2.196 had removed that default on the API path only. The box now starts unticked (#228, reported by @stefanelul2000).
- Three counters sat on zero whatever the data said: the number of users on the audit page, the certificates of each SSH CA, and the orders of each ACME account on the dashboard. Each tile read a field the API never sent, and each now reads a field it answers.
- The buttons on an ACME domain row were cut off on anything but a very wide window, and on a tablet they were not drawn at all. They were an ordinary column, so the table sized them from a guess made on the name of the key and hid what did not fit, while the card view drops plain columns entirely. They are row actions now: a column of their own, sized by the buttons it holds, and a menu on the card. The card shows the approval state too, which it used to leave out (#355, reported by @dogarethebest).
- That column was one pixel wide on every list, not just this one, because the data columns shared the whole width and left nothing for it. The buttons were drawn over the last column. Lists now reserve it, and the ACME domain columns, sized by the same guesswork, no longer cut their status badges either.
- A list sits flush against the edges and a card view keeps its margin, rather than each tab deciding for itself.
- The packages declared a licence the project does not have: the RPM named only BSD-3-Clause without the Commons Clause that goes with it, and the frontend package declared ISC, which it never was. The LICENSE file also called the result "open source", which a clause restricting sale is not.
[2.231] - 2026-09-15
Changed
- A request the server honours on other terms than the ones asked for now says so. Shortening a validity was the silent case: a policy ceiling or the issuing CA's own expiry cut the requested duration, the answer was still a success, and the only trace was an expiry date nobody reads until it matters. Issuance, CSR signing and renewal return what was granted, what was asked for and which policy or which CA expiry decided, and the interface shows it next to the result.
- Deleting a certificate that is still valid explains, before anything is deleted, that it has to be revoked first so the CRL and the OCSP responder carry the withdrawal, and offers to revoke and delete in one step. The refusal used to arrive after the fact, as a failure message, from both the list page and the detail window.
Fixed
- Delivery history grew without end. Every webhook delivery, deployment delivery and notification log entry was written once and deleted by nothing: no retention rule, no scheduled task, and not even deleting the webhook it belonged to, which left its whole history behind pointing at an identifier that no longer named anything. On one instance three months of ordinary use had left 47 MiB of webhook payloads, about two thirds of the database, because a payload carries the certificate it describes. Finished rows are now removed past a window that an administrator can change, thirty days for the ones that were delivered and ninety for the ones that failed, since a failure is what gets opened when someone asks why a notification never arrived. A delivery still owed is never removed, whatever its age, and a window set to zero keeps that kind for ever.
- An upgrade logged
Found 1 pending migration(s)and then went quiet, and the lines naming the migration and confirming it appeared only at the next shutdown, attributed to the process that had already finished. The migration had run; the progress line carries no newline of its own, so an unflushed stream left the journal holding it. The runner now writes each line through (#350, reported by @JoseGoncalves). - A certificate asked for three years came back valid for ninety days. Five example issuance policies were seeded active and without a scope on installations upgraded from an older release, and their ceilings contradict each other, so once the rules started being applied at issuance the lowest of them capped every request. The examples an administrator never adapted are switched off; anything renamed, given a scope, rewritten or created by hand is left as it is.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.233
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.233
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.233Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.233/ucm_2.233_all.deb
sudo dpkg -i ucm_2.233_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.233/ucm-2.233-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.233-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.233_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation