What's Changed
Security
- Direct private-key export now requires an admin-only scope — exporting a certificate's private key (
include_key, or apkcs12/pfx/key/jksformat) required onlywrite:certificates, which theoperatorrole holds by default — the very role that would otherwise go through the approval-gated Key Recovery flow. That made the four-eyes approval pointless: an operator could simply export the key directly, unapproved. Private-key export is now gated behind the admin-onlyread:private_keysscope; roles without it must use Key Recovery. CA private-key export was already restricted (write:cas) and is unchanged. Upgrade note: operators can no longer export certificate private keys directly — grantread:private_keysto a role (or use an admin account / admin-scoped API key) where direct export is required, otherwise route it through Key Recovery. Reported in #232. - ACME finalize rejects certificate requests carrying identities the order never validated — only DNS and IP subject alternative names were compared against the order's identifiers, and the CSR's SAN extension was copied verbatim onto the issued certificate: a client that legitimately passed a challenge for its own domain could also smuggle an email address (rfc822Name) or a Windows UPN (otherName) into the certificate — S/MIME impersonation, or AD logon as another user against a CA trusted for smartcard logon. Every SAN entry must now match a validated order identifier; any other general-name type is refused with a
badCSRerror. Contributed by @heidrickla (#242). - Key strength is enforced on every issuance path — the RSA ≥ 2048 / P-256+ floor documented for enrollment was only applied by SCEP and EST; a weak-key CSR (e.g. RSA-1024) was signed without complaint through the admin Sign-CSR endpoint and ACME finalize. The floor now lives in the signing service itself, so all paths inherit it. Upgrade note: weak-key CSRs now fail everywhere with an explicit message; there is no opt-out — re-key the requesting system. Contributed by @heidrickla (#243).
- CSR-supplied Extended Key Usages are capped to the certificate type's profile — whatever EKUs a CSR carried were copied onto the certificate (minus only OCSPSigning/timeStamping), so a protocol enrollee could mint itself a code-signing certificate through ACME. Protocol paths now honor only the EKUs the resolved certificate type permits;
anyExtendedKeyUsageand Smartcard Logon are never honored from a CSR. Operators keep full control through the explicit extra-EKUs option when signing. A follow-up closes the resulting corner cases: a CSR whose EKUs are all disallowed no longer yields a certificate with the EKU extension omitted entirely (which would be unrestricted) on any issuance path, SCEP included, and renewals preserve the certificate's existing EKUs at par instead of narrowing them to the default profile (#254). Contributed by @heidrickla (#243). - Sub-CA creation through Sign-CSR is gated and pathLen-clamped — signing a CSR as
intermediate_caonly required certificate-signing permissions and copied the CSR's BasicConstraints (pathLen included) unchecked. It now additionally requireswrite:cas, and the sub-CA's pathLen is clamped below the parent's (refused outright under a pathLen-0 parent), per RFC 5280 §4.2.1.9. A follow-up extends the same clamp to sub-CAs created viaPOST /cas(previously left unbounded under a constrained parent) and to the default CSR shape that carries no BasicConstraints (#254). Contributed by @heidrickla (#243). - The private-address guard applies to ACME IP-address orders — both challenge validators skipped the SSRF check for RFC 8738
ipidentifiers (it was gated on the identifier being a DNS name), so a deployment that had explicitly disabledallow_private_ipsstill fetched challenges from private and link-local addresses, including cloud metadata endpoints. Deployments using the default (private IPs allowed — the on-prem use case) are unaffected. A follow-up additionally refuses cloud-metadata and link-local targets unconditionally — even when private IPs are allowed — and rejects malformed DNS identifiers before validation (#256). Contributed by @heidrickla (#244). - Missing encryption keys are loud, and can be made fatal — without a configured database encryption key, integration secrets are protected by a key derived from the machine id (world-readable, routinely captured in backups and images), and without private-key encryption enabled, CA and certificate keys are stored unencrypted — both silently. Both conditions are now logged at startup, and two opt-in environment variables (
UCM_REQUIRE_DB_ENCRYPTION_KEY,UCM_REQUIRE_KEY_ENCRYPTION) refuse to run without explicit keys. Upgrade note: startup behavior is unchanged for existing deployments and existing data stays readable; the new startup error on installs that never enabled private-key encryption is informational — enable it under Settings → Security. A follow-up makes the require-flags refuse at startup (rather than on first secret use), treats an invalid key as fatal instead of silently falling back to plaintext, refuses the disable-encryption path up front, and accepts1/yes/onlike every other toggle (#252). Contributed by @heidrickla (#245). - mTLS, API-key, TSA, CSRF, SSH and OCSP hardening — issuing an mTLS client certificate now requires
write:user_certificates(it accepted any authenticated role, and fell back to an arbitrary CA when none was configured); API keys are capped at auth time to their owner's current effective permissions, so a key minted by a since-demoted operator no longer keeps its old scopes (upgrade note: keys whose owner lost roles or groups lose those scopes immediately — re-mint under an appropriate owner if an integration breaks); the mTLS enrollment endpoints lost their CSRF exemption; SSH certificate extension requests are validated against the CA's configured extension list; OCSP nonces are bounded per RFC 8954; a new opt-in TSA setting can require a dedicated timestamping certificate (the endpoint itself remains public, as RFC 3161 requires); and 2FA recovery codes work again — login compared a truncated prefix, so account recovery by code always failed. A follow-up expands a storedread:*/write:*API-key scope to the owner's current permissions instead of dropping it on demotion, preserves an explicitly empty SSH extension policy (it was collapsing back to the full default set), and requireswrite:cason the explicit-CA mTLS path (#253). Contributed by @heidrickla (#246). - Deployment and CI defaults hardened — the standalone database initializer now flags the seeded admin for a forced password change like every other bootstrap path; the Docker environment example no longer ships a working database password; the development compose file binds the debugger to loopback; the Trivy CI action is pinned to a release SHA; OPNsense imports verify TLS by default (upgrade note: API callers that relied on the old default must pass
verify_ssl: falseexplicitly — the UI checkbox is unchanged); and EST behind a TLS-terminating reverse proxy requires an explicit verification-success header instead of treating its absence as success (upgrade note: proxies must forwardssl_client_verify; clients fall back to HTTP Basic authentication otherwise). A follow-up completes the compose fix (the base file still published the application ports on 0.0.0.0), aligns the OPNsense verify-TLS default in the UI, and repairs a destructive database-reset path that dropped the schema without reseeding an admin (#255). Contributed by @heidrickla (#248). - The ACME Terms-of-Service text no longer interpolates HTML — in the admin preview or on the public terms page — the preview was built by a hand-rolled regex sanitizer whose output went to
dangerouslySetInnerHTML; it did not escape double quotes and never validated link schemes, so admin-supplied ToS text was stored XSS against any operator viewing the ACME settings page. The public, unauthenticated/acme/termspage rendered the same stored text server-side with the identical missing-quote escape, so a quote in a ToS URL could inject attributes there too. The preview now renders through ReactMarkdown with an http/https/mailto scheme allowlist (bare-URL autolinking, single-newline line breaks and list styling are preserved), and the server-side renderer escapes quotes before autolinking. Both are defence in depth — the CSP already blocked inline script execution on modern browsers. Contributed by @heidrickla (#247). - ACME finalize now validates the CSR subject, not just its SANs — the SAN-type check added in #242 was bypassable through the certificate subject. A CSR with no SAN extension still had a SAN synthesized from its subject when signed, so an order that only proved a DNS name could finalize with
emailAddress=ceo@victim.comin the subject and receive a certificate asserting that address — an identity the account never proved, which email-based client-certificate identity mapping (mTLS, EAP-TLS, VPN) trusts. Separately, only the first commonName was ever compared to the order, so a secondCN=admin.victim.comrode onto the certificate unvalidated — and because the name UCM stores and displays is read from the reversed RFC 4514 subject, that smuggled name even became the identity shown for the certificate. Finalize now requires every commonName to be a validated order identifier and refuses any emailAddress in the subject, with the samebadCSRerror. Contributed by @heidrickla (#251).
Fixed
- User groups can be given permissions from the UI, and the help text is accurate — group permissions have been enforced since 2.201 (a member's effective permissions are their role's unioned with those of their groups), but the group form only exposed name and description, so there was no way to actually set them — and the Users/Groups and RBAC help described a non-existent "assign a role to a group" model. The group form now has a permission picker (limited to the grantable set, never
admin:*/*), the group detail panel lists a group's permissions, and the help now describes how group permissions actually work. Reported in #233. - A certificate with more than one commonName is recorded under the right one — the stored and displayed
subject_cnwas taken by splitting the subject's RFC 4514 string on,and keeping the firstCN=token, but that format emits RDNs in reverse order, so the token found was the last commonName. A certificate whose subject wasCN=web.example.com, CN=admin.example.comwas listed, searched and exported asadmin.example.com. The same split also ignored RFC 4514's\,escape, so a comma inside another attribute (O=Example, Inc.) could truncate the name. Both recorders — the admin signing path and SCEP — now read the commonName from the subject itself, which is also how ACME validates it, so the name checked and the name recorded can no longer disagree. Existing rows are unchanged; re-issuing a certificate corrects its recorded name. Contributed by @heidrickla (#257). - Backend error messages reach the UI again — RFC 7807
application/problem+jsonerror bodies were read as plain text by the frontend's content-type check, so the backend's specific message (e.g. which password rule failed at user creation) was discarded in favor of a generic per-status string. Reported in #234, contributed by @Hemsby (#237). - Settings save buttons only submit their own section — every Save button on the General, Security, Backup and Audit tabs sent the entire settings object, so an invalid field in one card silently blocked saving an unrelated one, and the Password Policy and 2FA cards had no Save button of their own at all. Each button now owns exactly its card's fields, and both cards got their own button. Contributed by @Hemsby (#238).
- The Microsoft CA connection Save button carries its icon again, matching the other buttons in the row. Contributed by @Hemsby (#239).
- CRL scheduler no longer errors on offline CAs — a CA taken offline in password-protected mode keeps its (passphrase-encrypted) key in the database, so the scheduler still selected it for CRL regeneration and raised "Password was not given but private key is encrypted" on every cycle. Offline CAs are now skipped (with a debug log), for both base and delta CRLs, consistent with every other signing path.
Added
- Per-EAB domain restrictions — an EAB credential can be limited to the domains it may request certificates for:
*(any),*.example.com(all sub-domains), or an explicit list; an empty list blocks issuance entirely. Enforced on ACME new-order and new-authz, on both the built-in ACME server and the proxy. Existing credentials stay unrestricted (*) after upgrade and new credentials default to unrestricted, so nothing breaks. Restrictions are only meaningful when EAB is required (acme_eab_required), since otherwise clients can register without an EAB credential (migration 069). Implements #241, contributed by @gb-123-git (#259). - SCEP profiles — multiple enrollment endpoints — named profiles served at
/scep/<profile>/pkiclient.exe, each bound to its own CA, optional certificate template (whose key usage, extended key usage and validity govern issuance), challenge password (encrypted at rest, with the same expiry window as the global challenge) and approval policy. Device certs, user certs or per-tenant enrollments each get a dedicated URL instead of sharing one global endpoint; the existing/scep/pkiclient.exeendpoints keep serving the global configuration unchanged. Managed from a new Profiles tab on the SCEP page (migration 068). Requested in #228, and groundwork for Intune dynamic challenge validation. - Delegated OCSP responder certificates renew automatically — a daily task re-issues a responder certificate before it expires (same key pair and extensions, renewed at par) and rebinds the CA's responder configuration to the new certificate, so a short-lived OCSP signing certificate (e.g. the 90-day system template) rotates without manual action. Enabled by default; can be disabled and the renewal window tuned via configuration. Requested in #226.
- Template key type and validity are honored at issuance via the API — an issuance request carrying a
template_idbut nokey_type/validity_daysnow inherits them from the template (the UI already prefilled them); explicit request values still win. Requested in #226.
📜 Recent release history (last 2 versions)
[2.203] - 2026-07-23
Fixed
- TSA works again for existing installations — 2.200 required the timestamp signer to carry a critical, exclusive
timeStampingEKU, but the signer is the configured CA's own certificate, which carries no EKU at all: every deployment that timestamped before the upgrade got a 503 on all requests, and the product offered no way to mint a compliant signer. A CA certificate is accepted again (with a recommendation to use a dedicated TSA certificate), the newtsa_enabledgate no longer refuses installs that configured a TSA CA before the flag existed, and a client-pinnedreqPolicyis honored by issuing under that policy (RFC 3161 §2.4.1) instead of rejecting. - ACME issuance no longer fails closed on CAA DNS errors — 2.200 turned any CAA lookup failure (SERVFAIL, timeout, unreachable resolver) into a denial and terminally invalidated the order, which broke every renewal on air-gapped and split-horizon networks. DNS errors are non-blocking again by default (a warning is logged); strict fail-closed behaviour is available with the new "fail issuance on DNS lookup errors" toggle, and a transient DNS error never invalidates the order anymore. An RFC 8657
validationmethodsparameter is no longer enforced when the validation method is indeterminable (reused or auto-approved authorizations). - Renewals are no longer blocked by CA name constraints — 2.200 started validating renewals against the chain's NameConstraints, so certificates legitimately issued before enforcement (or before constraints were tightened) became un-renewable and fleets stranded at expiry; scheduled auto-renewal even swallowed the failure silently. Renewal-at-par now graces the names a certificate already carries (single, bulk, scheduled, SCEP and EST renewals) — only names new to the request are enforced — and auto-renewal failures are recorded in the audit trail. A CN that is not hostname-shaped (e.g. a person's name) is no longer treated as a DNS identity, and an unparseable NameConstraints extension no longer blocks all issuance from that CA.
- SCEP accepts legacy clients again — 2.200 required a 16-byte senderNonce and an in-window signingTime, and matched the RecipientInfo byte-for-byte against the CA certificate, which rejected fleets of already-enrolled devices (pre-RFC clients, devices without NTP, embedded stacks that re-encode the issuer). All three checks are tolerant again by default (warnings are logged); signingTime enforcement and its clock-skew window are configurable. GetCACert on an intermediate CA returns the single CA certificate again — the
application/x-x509-ca-ra-certchain response broke Apple/MDM enrollment (error -67731) — with the full-chain response available as an option. A renewal no longer silently strips EKUs the device's current certificate already carries. - EST accepts legacy integrations again — 2.200 returned 415 unless the request carried
Content-Type: application/pkcs10(breaking plain curl scripts) and refused PEM-encoded CSR bodies; both are accepted again with a deprecation warning. Re-enrollment no longer rejects a CSR whose SubjectAltName entries are reordered or differ only in criticality, and a CSR without SAN is issued as before. Clients that need the CA chain in the enroll response (instead of/cacerts) can opt in. - Syslog forwarding keeps line framing on upgrade — 2.200 silently switched TCP+TLS syslog to RFC 6587 octet-counting, corrupting the stream for collectors that only speak newline framing, with no way back from the UI. Existing configurations are pinned to line framing on upgrade (migration 066) and the framing is now an explicit setting.
- Revoked certificates can no longer answer
goodfrom the OCSP cache after upgrade — cache entries written before 2.200 (whose revocation-invalidation was broken) are purged once at upgrade (migration 066). - Wildcard ACME authorizations created before 2.200 are reusable again — they were stored in a format the new reuse lookup never matched, forcing a dns-01 re-validation on the first renewal (fatal with certbot --manual). Existing rows are normalized at upgrade (migration 067).
- UCM-managed ACME renewals recover when the upstream rejects
replaces— the RFC 9773 renewal hint is retried without the field on rejection instead of failing every renewal cycle until expiry. - A failed MS CA rekey submission no longer leaves the certificate paired with the wrong key — the freshly generated key/CSR are rolled back when the submission errors or is denied, so a PKCS#12 export cannot bundle a key that never got signed.
- Admin Sign-CSR can issue delegated OCSP responder and TSA certificates again — 2.200 silently stripped the
OCSPSigning/timeStampingEKUs on every path, including the operator explicitly signing a responder's CSR; the strip now applies only to protocol enrollees (ACME/EST), is logged, and can no longer be smuggled back in through the extra-EKUs merge. - OIDC provider settings are fully configurable in the UI — 2.200 enabled ID-token verification by default but the issuer, JWKS URI and verification toggle it relies on were never exposed, so existing OIDC providers failed closed with no way to fix them without manual API calls (#227).
Added
- Settings previously configurable only by direct database edits now have UI and API coverage: Certificate Transparency SCT embedding and the require-SCTs policy (with an updated default log list), OCSP response validity window, CAA issuer identifiers and enforcement mode, SCEP signingTime enforcement/clock skew/GetCACert chain, EST chain-in-response, syslog TLS verification and TCP framing, and the ACME client's TLS-ALPN-01 port. TLS-ALPN-01 (RFC 8737) is selectable when requesting certificates through the ACME client.
- Templates now govern the issued certificate's Key Usage and Extended Key Usage — issuing from a template previously ignored its configured KU/EKU entirely: the certificate type's built-in profile was applied instead and its EKUs were always merged in, so a template restricted to
OCSPSigningstill produced a certificate carryingserverAuth. When a template is selected itsextensions_templateis now the source of truth (extra EKUs are still added on top), and the certificate type selector in the issue form is disabled since the template defines it. Reported in #226. - Changing a template's type now updates its Key Usage / Extended Key Usage checkboxes — the type selector previously changed nothing in the form, so every new template started from the web-server defaults regardless of the chosen type. Reported in #226.
Added
customcertificate type at issuance — imposes no Extended Key Usage: only the EKUs you pick end up in the certificate, and with none selected the EKU extension is omitted entirely. This makes single-purpose certificates (e.g. an OCSP responder with onlyOCSPSigning) possible from the issue form. Requested in #226.- OCSP Signing system template (
ocsp_signingtype) — delegated OCSP responder profile (RFC 6960):digitalSignature+OCSPSigningonly, 90-day validity. Existing installations receive it automatically on upgrade.OCSPSigningis also selectable in the template editor's EKU checkboxes. Requested in #226.
[2.202] - 2026-07-22
Fixed
- Security keys stopped working after upgrading to 2.200 — signature-counter clone detection began being enforced against a stored value that earlier releases had incremented on every login, including for the many authenticators (platform passkeys, and most FIDO2 keys) that always report a counter of zero. Those users could never present a value high enough and were locked out of their own key with
Authentication failed. Clone detection is now only applied when the authenticator actually maintains a counter, and the inflated stored values are reset once on upgrade (migration 065); a genuine counter that fails to increase is still refused. - Test runs no longer write to the production log — the suite inherited the native logging configuration and appended to
/var/log/ucm/ucm.log. On a host that also runs UCM this polluted real logs and, when the suite ran as a different user, took ownership of the file at rotation — after which the service could no longer write its own log at all. - Access log was never written — the WebSocket-capable Gunicorn worker bypassed Gunicorn's access logger entirely, so
/var/log/ucm/access.logstayed empty on every native install no matter how many requests were served. This made it impossible to tell whether a protocol client (SCEP/EST/ACME) had even reached the server when troubleshooting. Requests are logged again. - EST and TSA configuration refusals are now logged — same gap as SCEP: an unknown EST label, an unconfigured EST CA, a disabled TSA, and a TSA CA that is missing or has no certificate/key all returned an error to the client without recording anything. Contributed by @Hemsby (#224).
- SCEP configuration refusals are now logged — a request that reached the server and was turned away by a configuration check (CA offline, no private key, HSM-backed CA, no CA assigned) returned a 500 without recording anything, which was indistinguishable in the log from a request that never arrived. Each refusal now names its reason and the CA involved. Reported by @Hemsby.
- Inbound SCEP requests are now logged — only failures were recorded, so a silent log was ambiguous between "the device never reached us" and "it reached us and succeeded". Each request now logs its operation, HTTP method, source address and client User-Agent.
Full history: CHANGELOG.md
Installation
Docker (Recommended)
# From Docker Hub
docker pull neyslim/ultimate-ca-manager:2.204
# Or from GitHub Container Registry
docker pull ghcr.io/neyslim/ultimate-ca-manager:2.204
# Run
docker run -d -p 8443:8443 \
-e SECRET_KEY=$(openssl rand -hex 32) \
--name ucm neyslim/ultimate-ca-manager:2.204Debian/Ubuntu
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.204/ucm_2.204_all.deb
sudo dpkg -i ucm_2.204_all.deb
sudo apt-get install -fFedora/RHEL
wget https://github.com/NeySlim/ultimate-ca-manager/releases/download/v2.204/ucm-2.204-1.fc43.noarch.rpm
sudo dnf install ./ucm-2.204-1.fc43.noarch.rpmSilent/Automated Install
# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.204_all.debDefault Credentials
- Username:
admin - Password:
changeme123
Change the password immediately after first login!
Documentation
- Installation Guide
- API Documentation