github NeySlim/ultimate-ca-manager v2.187

latest releases: v2.239, v2.239-rc1, v2.238...
3 months ago

What's Changed

Added

  • Configurable public vhost for ACME directory URLs — a dedicated public hostname/port (acme_public_vhost, acme_public_port in Settings → General) can now be advertised in the local ACME server and ACME proxy directory URLs (/acme/*, /acme/proxy/*), independent of the admin UI URL — the split admin/ACME reverse-proxy topology (e.g. admin.ucm.example.com vs acme.ucm.example.com behind one wildcard certificate). JWS verification accepts both the advertised public origin and the inbound URL; without a configured vhost, behavior is unchanged (request host). Wildcard hostnames are rejected for the vhost value (TLS SAN concept, not an advertised URL), and an optional acme_public_tls_cert_id records which managed certificate to deploy on the ACME vhost (metadata only). Settings APIs expose acme_public_base_url / acme_proxy_public_base_url and the UI directory URLs follow the configured origin (#173, thanks @fredlubrano).

Changed

  • SQLAlchemy 2.0 Session.get — all 353 Model.query.get() / query.get_or_404() call sites migrated to db.session.get() / db.get_or_404(), eliminating ~4500 LegacyAPIWarning per test run ahead of a future SQLAlchemy major bump.

Security

  • CSR extension policy on certificate issuance — the shared CSR signer copied every requested extension into the issued certificate verbatim, so a crafted CSR carrying BasicConstraints CA:true (and keyCertSign) submitted through EST, SCEP or ACME enrollment could obtain a working subordinate CA — a trust escalation from an enrollment endpoint. Leaf certificate types now force BasicConstraints(ca=False) and strip the CA-only key-usage bits; only the explicit intermediate-CA signing flow may assert CA powers. Regression tested.
  • SCEP unauthenticated auto-enrollment when no challenge is set — per RFC 8894 §2.4 an omitted challengePassword allows unauthenticated authorisation, so with auto-approve enabled and no challenge configured any anonymous client on the public SCEP endpoint received a CA-signed certificate. Initial PKCSReq auto-issuance is now refused unless a challenge is configured (manual-approval mode and renewals are unaffected); UCM_SCEP_ALLOW_NO_CHALLENGE=1 opts back in for isolated deployments.
  • Key-strength floor on EST and SCEP enrollment — both protocols now reject CSRs whose public key is below policy (RSA < 2048, non-NIST EC curves) instead of signing weak/exotic keys, matching the UI/API issuance floor (EST RFC 7030 §3.7 and SCEP defer key policy to the local CA).
  • EST request-body cap bypass via chunked encoding — the body-size limit only inspected Content-Length, so a Transfer-Encoding: chunked request could stream an unbounded body into memory. The body is now read with a hard cap on the request stream regardless of framing.

Fixed

  • Broken settings restore endpoint — POST /api/v2/settings/backup/restore passed a temp-file path where the service expects raw bytes, so it returned 500 on every call and left the uploaded (encrypted) backup in /tmp on each attempt. It now reads the upload as size-capped bytes in memory (no temp file) and restores correctly.
  • mTLS certificate import always failed — POST /api/v2/mtls/enroll-import crashed on every valid PEM (undefined variable, then a str stored into the binary cert_pem column). The endpoint now imports and enrolls correctly; covered by a regression test.
  • Public ACME vhost hardening (post-#173 review) — the local ACME server now accepts the JWS url on both the advertised public origin and the inbound request origin (the tolerance moved into verify_jws itself, shared with the proxy, so in-flight orders survive an acme_public_vhost change on /acme/* too); CAA enforcement and caaIdentities follow the configured public hostname instead of the inbound Host; the vhost is validated as a real FQDN (rejects .., leading/trailing hyphens, single labels); non-string vhost values and out-of-band garbage port rows return 400/defaults instead of 500; the public origin is memoized per request (one combined SystemConfig read instead of up to 10 per proxy request); the TLS certificate is picked from a dropdown of key-bearing certificates instead of a raw database id, and clearing it removes the config row; the CA Accounts panel shows the same public directory URLs as the other tabs.

📜 Recent release history (last 2 versions)

[2.186] - 2026-07-06

Fixed

  • ACME proxy /directory on fresh install — the legacy /acme/proxy/directory and /new-nonce endpoints returned a 500 (No external ACME CA account configured for the proxy) on a brand-new instance before any external CA account was added, because the proxy service resolved the upstream AcmeClientAccount eagerly in its constructor. Resolution is now lazy: only the upstream directory URL (config-derived, defaults to Let's Encrypt staging) is needed for /directory and /new-nonce, and the account row is looked up on the first key-bearing operation (new-account, new-order, signing), preserving the helpful configuration error for actual signing paths. This had broken the release smoke gate on a fresh Docker container.
  • ACME manual verify DNS wait and authorization poll cadence — the synchronous manual-verify endpoint ran the DNS self-check up to the full configured dns_propagation_timeout (max 3600 s), risking client/proxy timeouts; it is now capped at 30 s (background auto-poll still honors the full timeout). The auto-poll loop also queried per-domain authorization status on every iteration, doubling CA traffic; it is now checked every max(poll_interval × 5, 15) s, with the order-status poll still catching terminal invalid states each interval.
  • Contact-less upstream account registration — AcmeClientService.register_account now accepts email=None and omits the contact field (RFC 8555 makes it optional; Let's Encrypt accepts contact-less registrations), so the proxy no longer blocks issuance when the only available email has a non-public TLD (.lan/.local).
  • ACME preflight staging order cleanup on failure — an ephemeral staging order created during a full preflight is now removed from the database even when a later stage of the preflight raises, instead of leaking an orphan row.
  • Key-reuse renewal fallback — key_source=reuse now falls back to the original source_certificate_id when the order's own certificate_id is missing (e.g. an order whose import failed), so the key-reuse chain survives across renewals.

[2.185] - 2026-07-06

Added

  • ACME proxy multi-CA with per-account slug endpoints — the ACME proxy upstream is now selected per external CA account (AcmeClientAccount), so several CAs (Let's Encrypt staging, production, ZeroSSL, …) can be exposed in parallel. Each account can opt in to the proxy with a unique slug, served at /acme/proxy/<slug>/directory alongside the legacy /acme/proxy/directory (backward compatible). Reserved slugs (directory, new-order, acct, challenge, …) prevent collisions with existing routes. Migrations 050 (acme.proxy.acme_account_id) and 051 (proxy_enabled, proxy_slug) backfill the account already linked in proxy settings. The UI surfaces a toggle and slug field on external CA accounts and lists all enabled endpoints with a Certbot example (#170).
  • Typed SAN validation for certificate issuance — the Issue Certificate form (POST /api/v2/certificates) now validates SAN entries per type (DNS, IP, Email, URI, UPN) on both backend and frontend, with cross-type errors (e.g. an FQDN entered in the IP field is rejected with a hint to use DNS) and ECDSA curve mapping (256/384/521 → prime256v1/secp384r1/secp521r1). The CN auto-SAN no longer adds an email CN as a DNS SAN on server certs; email SAN is added only for Email/Combined types. New RSA 3072 size and P-384/P-521 curve options appear in the Issue Certificate selector (#169).
  • CI: block bot Co-authored-by trailers — a new no-bot-attribution.yml workflow rejects PRs whose commits carry Co-authored-by trailers from known AI agent identities (Cursor, Copilot, Claude, …), so commits attributed to bots can no longer reach dev.

Fixed

  • ACME DNS-01 propagation diagnostics and dns_propagation_timeout=0 — the DNS-01 self-check succeeds as soon as the authoritative (or configured) resolver confirms the TXT record, but the per-public-resolver diagnostic log was emitted even on success and a flaky resolver (e.g. SERVFAIL from Quad9) was indistinguishable from a real propagation gap. Each failing public resolver is now logged at DEBUG with its exception type (NXDOMAIN / Timeout / ConnectionError / …) and the public-propagation line is explicitly marked (diagnostic, does not block issuance). dns_propagation_timeout=0 now skips the pre-check entirely on both the auto-poll background path and the manual Verify path (was a single-pass probe), matching the "submit immediately" help text (#171).
  • TXT RDATA multi-string concatenation — long ACME authorization tokens published as a single TXT RR carrying several <character-string> elements (RFC 1035 §3.3.14; Quad9 splits them across quoted strings) are now joined before matching against the expected value. A correctly published record no longer renders as value_mismatch / pending on such a resolver (#171).
  • Migration 050 NotNullViolation on PostgreSQL — the proxy account backfill inserted legacy credentials into acme_client_accounts without created_at/updated_at; on instances where those columns are NOT NULL without a server DEFAULT, the raw INSERT failed at boot (null value in column "created_at"). The migration now provides both timestamps explicitly on the SQLite and PostgreSQL paths.
  • Flaky OCSP/CDP auto-URL tests — CA OCSP/CDP/AIA auto-URL generation relied on hostname -f via _get_fqdn(), which on CI runners can return a short hostname without a domain and resolve to None, flaking the update-CA tests. The shared test app fixture now pins FQDN = 'ucm.test' so URL generation is deterministic.

Full history: CHANGELOG.md


Installation

Docker (Recommended)

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

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

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

Debian/Ubuntu

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

Fedora/RHEL

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

Silent/Automated Install

# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.187_all.deb

Default Credentials

  • Username: admin
  • Password: changeme123

Change the password immediately after first login!

Documentation

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

NewReleases is sending notifications on new releases.