What's Changed
Added
- Automatic updates — a daily background check on your chosen channel surfaces new versions in the UI and through the notification pipeline (new
system.update_available/system.update_initiated/system.update_installed/system.update_failedwebhook events), and an opt-in unattended install (off by default) downloads, verifies and installs the update at a configurable hour, with a full audit trail. Channels are strict:stabletakes final releases only andrcadditionally takes rcN prereleases only — alpha/beta/dev tags never qualify for either. The install outcome is truthful end-to-end:system.update_initiatedfires when the checksum-verified trigger is written, then the privileged update watcher installs the package, verifies the installed version against the target and records a durable result;system.update_installedorsystem.update_failed(with the failing step) is emitted from that result after the restart — never before the install actually happened. The unattended install refuses to run when its anti-repeat marker cannot be persisted, and the settings PATCH is transactional (an error is reported instead of a partially saved config). DEB/RPM installs only — Docker deployments get the check and notification with a pointer to pull the new image (#301) - Update downloads are now verified against the release's published SHA256 checksum — a manual install refuses to proceed when the published checksum can't be retrieved or doesn't cover the package, and an unattended install additionally requires a checksum to exist; RPM releases now publish a
.sha256next to the package like DEB releases already did (#301) - Webhook event names shown in the webhook form and list are now localized in all 9 languages
- External-CSR CAs: installing a renewal certificate now reports the superseded certificate's serial and expiry (API response, notice in the upload dialog, audit log) as a reminder to revoke it at the external root — UCM cannot publish that revocation itself, since the issuer's key is not held here (follow-up to #298)
- Deploy hooks — push issued and renewed certificates to remote hosts over SSH. Admin-only deploy targets (Settings › Deployment) hold key-based SSH credentials (encrypted at rest, ed25519 key generated by UCM or imported, host key pinned on first connect with a UI action to reset the pin after a legitimate host reinstall) and one fixed reload command; certificates are attached to targets from the certificate detail view with per-file destination paths (certificate / private key / full chain — distinct paths enforced — written atomically over SFTP with restrictive key permissions). Attaching a target queues the first deployment immediately; renewals redeploy automatically. Pushes run asynchronously through a durable delivery queue with retry/backoff (per-delivery status, manual deploy-now and retry, full audit trail). Deleting targets/bindings requires the
delete:deployscope. Use a dedicated low-privilege SSH account on each target (migration 081, new dependency paramiko) (#299) - Externally-signed CRLs for offline / key-less CAs — a CRL generated next to an offline root (file-exported offline CA, certificate-only import, external-CSR parent) can now be uploaded (
POST /api/v2/cas/<id>/crl, PEM or DER, also from the CA detail view) and served at that CA's existing CDP endpoint. The upload is validated before being accepted: the signature must verify against the CA certificate, the issuer must match, and the CRL must be newer than the currently served one (CRL number / thisUpdate monotonicity). OCSP consults the uploaded CRL for that issuer's serials on every request shape (single and multi-CertID), the upload invalidates the CA's cached OCSP responses so a pre-generatedgoodnever outlives the CRL that revokes it, and the CA detail view shows thisUpdate / nextUpdate / entry count with a stale warning once nextUpdate has passed (migration 080) (#302) - OCSP for key-less CAs: the signing identity is now resolved delegated-responder-first, so a certificate-only import or an offline file-exported CA answers signed OCSP through a delegated responder kept online; without any usable key the responder answers
unauthorized(RFC 6960) instead ofinternalError(#302)
Fixed
- TSA: selecting the signing CA on the TSA settings page now persists — the page echoed the read-only
ca_idfield back to the API, which silently overrode the just-savedca_refid(selection wiped on first save, reverted to the previous CA when switching) while the UI still reported "saved" (#300)
📜 Recent release history (last 2 versions)
[2.214] - 2026-08-25
Security
- Certificates: a valid, non-revoked X.509 certificate can no longer be deleted — deletion removed the record while the certificate stayed trusted until expiry, with no CRL/OCSP trace; revoke it first so relying parties see the change (single delete returns 409, bulk delete reports the item as failed). Expired or revoked certificates, and CSR-only records, are unaffected (#296, contributed by @gb-123-git & PMGA Tech LLP)
- Revocations now survive certificate deletion — a new persistent
revoked_serialsstore keeps every revoked or superseded serial on the CRL and answeringrevokedover OCSP until the original certificate's notAfter, even after the certificate record itself is deleted. A revocation and its persistent record are written in one transaction, and removing a certificate hold deletes both atomically, so relying parties can never observe a half-applied state (migration 078) (#297, contributed by @gb-123-git & PMGA Tech LLP) - Renewing a revoked certificate is refused (409) instead of silently clearing its revocation state — issue a new certificate instead (#297, contributed by @gb-123-git & PMGA Tech LLP)
- Renewed private keys are encrypted at rest exactly as at issuance — bulk renewal previously stored them unencrypted, silently downgrading deployments with a master key configured (#297, contributed by @gb-123-git & PMGA Tech LLP)
Changed
- Certificate renewal is now in-place — manual, bulk and automatic renewal share a single implementation that updates the existing certificate record:
idandrefidnever change (API consumers and integrations keep their stored references),created_atpreserves the original issuance date, and newrenewed_at/renewed_timesfields track the renewal history. The superseded serial is published on the CRL (reasonsuperseded) and over OCSP until the old expiry. Certificates whose key UCM holds are re-keyed on renewal; protocol-enrolled certificates (SCEP/EST/ACME) are re-signed with their existing public key, since the private key lives on the client (#297, contributed by @gb-123-git & PMGA Tech LLP)
Added
- CA signed by an external CA (CSR) — a new CA creation mode for the canonical offline-root / online-issuing-CA pattern: UCM generates the key pair (local or HSM-backed) and a CA-flavored CSR (
basicConstraints CA:TRUEandkeyCertSign/cRLSign, both critical), and the CA waits in an "awaiting certificate" state with the CSR downloadable — the private key never leaves UCM. Uploading the certificate signed by the external root (PEM or DER) activates the CA after strict checks: the certificate's public key must match the stored private key, CA BasicConstraints/KeyUsage are enforced, the effective pathLen comes from the signed certificate, and the chain links automatically when the external root was imported (certificate only; AKI→SKI match with issuer-DN fallback, signature verified). Renewal re-issues a CSR from the same key so the SKI stays stable across renewals, and a pending CA is excluded from every signing, CRL, OCSP and protocol path until activated (migration 079). Smoke-tested end-to-end on SQLite and PostgreSQL. Requested in #298 - Optional auto-purge of stale revoked-serial records (expired ones only) during CRL generation — default off, so records are kept as renewal-chain history unless an admin enables it (#297, contributed by @gb-123-git & PMGA Tech LLP)
Fixed
- Auto-renewal notification settings are now honored — the notify toggles were never read back (notifications always sent) and boolean values did not survive a settings round-trip; malformed JSON in the auto-renewal configuration no longer crashes the scheduler (#297, contributed by @gb-123-git & PMGA Tech LLP)
[2.213] - 2026-08-19
⚠️ Upgrade note — in-flight ACME proxy orders placed with a bare JWK must be
re-created. The proxy'snew-ordernow requires a kid (RFC 8555 §6.2) and binds
every order to an RFC 7638 thumbprint. A pending proxy order created before this
release through a jwk-signed new-order carries only the legacy thumbprint of the
raw JWK dict, which no longer equals the value derived from the requesting key — and
the account-row reconciliation cannot resolve it either, because that legacy hash
never equals a stored account thumbprint. Those orders become inaccessible and must
be re-created. ACME automation self-heals (the client simply places a new order on
its next run), and already-issued certificates are unaffected — but a client that
polls a saved order URL forever will need a nudge.
Security
- ACME proxy
new-orderrequires a kid — a jwk-signed JWS is verified against its own inline key with no account lookup, so accepting one skipped the account existence/status check and the per-EAB identifier restrictions whenever EAB was not mandatory, and left the resulting order with no owner recorded — which the ownership check then served to every other client of that proxy. Identifier and challenge-type validation still runs first, so a client with an unsupported identifier keeps gettingunsupportedIdentifierrather than an authentication error (#260) - Deactivated and revoked accounts are refused on
new-order— the status check only covereddeactivated, so a revoked account could keep ordering certificates - Proxy resources require a positive owner match — authz, challenge, order and certificate access previously allowed any verified requester when an order's owner binding was half-populated (an account id without a stored thumbprint, or a thumbprint that resolved to no account): the absence of a contradiction was treated as authorization. A match is now required on one of the two fields, and a half-populated binding is reconciled through the owning
AcmeAccountrow rather than waved through (#260) - Owner thumbprints follow RFC 7638 — the thumbprint was computed over the raw JWK dict, so a client sending optional members (
alg,kid,use) turned a legitimate owner into a mismatch; only the required members are hashed now, by a single implementation shared with the value stored on the account - Upstream authorizations are matched exactly — the candidate lookup is a
LIKE '%url%'prefilter, which also matches a prefix of a stored URL (.../authz-v3/99inside.../authz-v3/999); a client could bind a foreign authorization to an order it does own and pass the ownership check with it. Prefilter hits are now re-checked against the decoded URL list - Challenge-to-order resolution has no approximate fallback — the owning order is resolved from the recorded authz mapping, from the challenge's parent authz path, or from the upstream
Link rel="up"header only. There is no host-only match and no "most recent pending order" guess, either of which could run DNS-01 automation against a foreign domain;rel="up"is also selected explicitly rather than taking the first link, sincerel="index"is commonly listed first - The certificate-download fallback scan is owner-scoped — resolving an untracked certificate URL scanned every pending proxy order and signed one upstream POST per candidate, letting any caller drive the proxy's upstream account (cross-tenant probing, rate-limit burn). The scan is now restricted to rows the requester could own and capped
- Ownership is verified before any upstream call — a denial no longer costs a signed request to the CA
- A challenge whose identifier cannot be determined fails closed — on a multi-SAN order the first domain is not necessarily the one being validated; publishing the DNS-01 TXT record under that name was wrong. The request now fails with a problem document naming the cause
- SSH certificates: a valid, non-revoked certificate can no longer be deleted — deletion silently dropped the record while the certificate stayed trusted by servers until expiry; revoke it first (the API returns 409 otherwise) (#292, contributed by @gb-123-git)
Fixed
- Azure Key Vault HSM provider: key generation passed an invalid
hsmargument tocreate_rsa_key/create_ec_keyand failed with aTypeError— the SDK keyword ishardware_protected(#295, contributed by @jeancarlor) - Discovery: large scheduled scans no longer exhaust memory and crash-loop the service — probes are submitted through a bounded window with connection-level failures discarded immediately, a scan is capped at 65,536 host×port probes (rejected with a clear error beyond), a profile's next-run timestamp advances when a scan starts (not when it completes), scans orphaned by a restart are marked failed at startup, and reverse-DNS lookups are now lazy (#293)
- Bulk Actions: pagination, search and status/CA filters for certificates and CSRs now run server-side against the complete inventory — the page-size selector is respected, selections survive page changes, resource count chips show real totals, and the CSR list endpoint gained server-side search (#294)
- Background DNS-01 threads no longer share ORM state with the request that started them — the worker rebound the request's account object to its own session, racing the request thread and the sibling threads a multi-domain order starts; each worker now signs through its own service instance
Changed
- Proxied ACME requests reuse the upstream directory, finalize URL and challenge mapping, and pool
Replay-Noncevalues — every proxied call previously paid an extraGET /directoryplusHEAD /new-nonceupstream. All caches are advisory: a miss falls back to the upstream round-trip, so a restart or a second worker costs performance, never correctness
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.215
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.215
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.215Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.215/ucm_2.215_all.deb
sudo dpkg -i ucm_2.215_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.215/ucm-2.215-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.215-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.215_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation