github NeySlim/ultimate-ca-manager v2.207

latest releases: v2.219, v2.219-rc2, v2.219-rc1...
20 days ago

What's Changed

Added

  • Native Windows autoenrollment (MS-XCEP + MS-WSTEP) (#229, contributed by @Hemsby) — UCM now serves the Active Directory Certificate Services enrollment protocols: MS-XCEP policy discovery (/ADPolicyProvider_CEP_* GetPolicies, advertising key usage, EKUs and enrollment URIs to Windows clients) and MS-WSTEP issuance/renewal (/ADCertificateService_CES_* RequestSecurityToken — RequestSecurityTokenResponseCollection). Three authentication bindings, mirroring real ADCS CES endpoints: WS-Security UsernameToken (with HTTP Basic fallback), client-certificate renewal (the RST must be XML-DSig-signed with the private key of a certificate UCM itself issued — byte-for-byte matched against the stored certificate), and Kerberos/SPNEGO over HTTP Negotiate (pyspnego + keytab, optional dependency). A new AD Connector (Settings) lets UCM query Active Directory over LDAP(S) — bind password encrypted at rest — to derive the subject of the deliberately-empty CSRs Windows GPO machine autoenrollment submits (machine: CN=<dNSHostName>; user, opt-in per template: the AD directory DN as subject plus a UPN OtherName SAN, with the AD mail attribute opportunistically added). Templates gain three opt-in flags: autoenroll_enabled (advertise autoEnroll=true in XCEP policy — the Enroll/Autoenroll split real ADCS enforces), ad_derived_subject, and allowed_ad_group for per-template Enroll ACL via AD group membership (nested groups included). Windows-specific interop details were verified against a live AD lab, including the TLS 1.2 cap required by some schannel clients (applied only while WSTEP is enabled), CMC full-PKCS#7 request unwrapping and response building, the Microsoft Certificate Template extension on issued certs (absence breaks CX509Enrollment), UPN SANs, and proof-of-possession enforced on PKCS#7/CMC-wrapped requests.
  • SSH certificate issuance UI — the certificate type selector now auto-syncs to the selected SSH CA (contributed by @gb-123-git).

Security

  • Community audit round-up (contributed by @gb-123-git, #263): path traversal in the update-download helper (unsanitized package_name reached a file path), path verification on backup download/delete (filenames are now resolved and confined to the backup directory), SQL-injection hardening of the PostgreSQL sequence-reset helper (compiler-rendered identifiers and bound names, per-column savepoints), missing ownership checks on ACME-proxy resources — account status enforced at new-order, DNS automation only triggers for the order's owner, challenge-to-order attribution fails closed when the upstream CA omits the Link rel="up" header (legacy orders recorded before ownership tracking intentionally keep read access) — IDOR fixes on user-certificate revoke/delete, SSO provider secrets no longer returned to non-admins requesting include_secrets, CA export password removed from the URL query string (out of access logs), LDAP bind passwords encrypted at rest, and security-sensitive settings (session/lockout/HSTS/public-URL/password-policy keys) now require admin:settings, with the settings UI locking those fields for operators instead of failing the whole card save.
  • WebSocket security overhaul (contributed by @gb-123-git, #263): socket handshakes previously skipped authentication entirely, so any connection received every certificate/CA broadcast, and authenticated clients could subscribe to arbitrary rooms. Connections now authenticate through the Flask session (or an API key scoped to its owner's current permissions, with mandatory periodic re-authentication) and fail closed on error, events are emitted to server-managed permission-scoped rooms only, clients cannot leave mandatory rooms or join privileged ones without the underlying permissions, subscriptions are rate-limited, sessions are periodically re-validated against the auth manager, and API-key authentication in the URL query string is deliberately unsupported.
  • WSTEP certificate-bound renewal requires the exact issued certificate — renewal originally recognized the signing certificate by serial number alone; serial and subject are public, so a forged self-signed lookalike bearing a victim's serial/subject could renew a victim's identity onto an attacker's key. The presented certificate must now match the stored certificate byte-for-byte for the configured CA, and the DB lookup tolerates all serial storage formats (serial_variants(); ARI reuses the same helper).

Fixed

  • Empty-subject CSRs with an IP-only SAN now populate the CN from that IP — the populate-CN-from-SAN fallback only looked at DNS names, so certbot-style CSRs whose single SAN is an IP address produced certificates with a genuinely empty subject (some clients, notably Safari on iOS, reject those) (#271, contributed by @Hemsby).
  • SSH CA download/copy public-key buttons and the missing openssh-client package in the Docker image for KRL generation (contributed by @gb-123-git).
  • Kerberos keytab-validation tests now skip when the optional gssapi extra is not installed (CI environments without the Kerberos libraries), rather than failing.

📜 Recent release history (last 2 versions)

[2.206] - 2026-08-08

Added

  • Optional purge of replaced ACME-proxy certificates (#240) — new opt-in ACME setting acme.proxy.prune_replaced_certificates (UI toggle under ACME → Let's Encrypt proxy): when a proxy order finalizes, certificates previously imported by proxy orders for the exact same domain set are deleted instead of piling up in the inventory. Revoked certificates are always kept, and certificates not issued through the proxy are never touched. Off by default. (Note: the proxy never stores the client's private key in the first place — keys stay client-side by design of ACME CSRs.)
  • CRL validity window extended to 5 years for offline CAs (#236) — validity_days now accepts 1–1825 (was 1–365) and the CRL/OCSP page offers 90d/180d/1y/3y/5y options, with a warning past one year since relying parties may keep stale revocation data for the full window. Intended for offline root CAs that cannot re-sign CRLs on schedule; online CAs should stay at short validity.

Fixed

  • Duplicate CA/certificate creation on double-submit (#269) — the Create CA modal and Issue Certificate form never disabled their submit button, so a double-click (or Enter+click) during a slow RSA keygen created two identical objects. Both forms now ignore re-entrant submissions and disable the button while one is in flight.

[2.205] - 2026-08-07

Security

  • ACME proxy endpoints now verify resource ownership — the proxy's authorization, challenge, order-status and certificate-download endpoints verified the JWS signature but never checked that the requesting account owned the resource: any account registered on the proxy could read any other account's authorization status and challenge details, poll their orders, and download their certificates by enumerating the base64-encoded upstream identifiers (the native ACME server was not affected — it has enforced this binding all along). Every order-scoped proxy endpoint now enforces the owner binding recorded at new-order (account id and/or client JWK thumbprint), the same check finalize already performed: a foreign account gets unauthorized (403), and an owner-bound resource whose requester identity cannot be derived fails closed. Because challenge and authorization URLs live in disjoint namespaces on most CAs, a challenge is attributed to its order through the authorization URL the upstream CA itself returns in the Link rel="up" header, never by guessing from the URL shape. Resources no longer tracked by any proxy order (e.g. the order row was deleted) now return 404 instead of being fetched upstream; orders created before ownership tracking existed carry no binding and are served as before. Reported by @gb-123-git (#260).
  • Trusted-proxy gate vs ProxyFix REMOTE_ADDR rewrite — with UCM_BEHIND_PROXY=1 (or UCM_TRUSTED_PROXY_HOPS>0), Werkzeug's ProxyFix rewrites REMOTE_ADDR from X-Forwarded-For before the trusted-proxy gate ran. Two consequences: requests arriving through a configured reverse proxy failed the check (every nginx-proxied Web UI request was 403'd with "Untrusted X-Forwarded-Host from non-proxy client", and mTLS / EST client-cert headers from the proxy were ignored); and a direct attacker who could reach the backend could pass the gate by sending X-Forwarded-For: 127.0.0.1, unlocking SSL_CLIENT_* / X-SSL-Client-* header handling used for client-certificate authentication. Trust decisions now key on the real TCP peer preserved in werkzeug.proxy_fix.orig (utils.trusted_proxy.immediate_peer_addr()), client_ip() trusts ProxyFix's hop-aware result when it rewrote REMOTE_ADDR instead of re-parsing client-supplied XFF entries (the legacy X-Real-IP fallback is kept when ProxyFix found nothing to apply), spoof-attempt warning logs report the real peer, and the mTLS middleware reuses the shared gate instead of its own duplicated (equally affected) check.

Fixed

  • Navigation highlight now derives the active item from the URL path — the active page was derived from the first URL segment under a "segment === nav item id" naming convention, so every route that breaks the convention never highlighted and its sidebar group never auto-expanded: /key-recovery (nav id keyRecovery) and the SSH pages /ssh/cas and /ssh/certificates (nav ids ssh-cas/ssh-certificates) were permanently unhighlighted, while scep-config, est-config and tsa-config only worked through a hand-maintained exception map. The active item is now resolved by matching each nav item's path (exact match first, then longest prefix so detail pages like /cas/123 still highlight their section), which keeps the highlight correct by construction for any future nav item. The Key Recovery contextual help panel, gated on the same broken id, now opens as well.
  • EST settings no longer reject a fresh CA selection when the previously configured CA was deleted — deleting a CA leaves its est_ca_refid value behind, and GET /api/v2/est/config keeps returning that stale refid (reporting ca_id: null); the settings page PATCHes the whole object back, so the stale refid arrived alongside the fresh ca_id and the update handler, which validated ca_refid first, answered CA not found before ever looking at the valid selection. When both fields are sent, ca_id is now authoritative; a ca_refid-only request is validated exactly as before.
  • SSH CA TTL fields accept the duration format the UI advertises — the create/edit placeholders say "e.g. 24h, 7d, 365d", but default_ttl/max_ttl were cast with a bare int(), so 365d was rejected with invalid literal for int() (and the import path stored the raw string, crashing later at issuance). All three entry points (create, update, import) now normalize through utils.duration.parse_duration_seconds() — plain numbers stay valid as seconds, s/m/h/d/w/y suffixes are accepted, and malformed values fail with a clear 400 naming the accepted formats.
  • Approved issuance (policy workflow) now honors the certificate template — when a deferred certificate request finalized after approval, the legacy approval issuance path ignored the template's Key Usage / Extended Key Usage, used hardcoded cert-type profiles, always signed with SHA-256 (ignoring the template digest), fell back to its own request defaults instead of the template's key type/validity, stored the resulting private key unencrypted, dropped the template_id linkage, and skipped the CA-expiration sanity check. The approval path now applies the template's extensions template / digest / defaults like the direct issuance path (#226), encrypts the issued private key via encrypt_private_key, preserves the template link with template_overrides (#258), and refuses validity beyond the CA's own end-of-life.

Added

  • Custom Command DNS provider for ACME DNS-01 — a new DNS provider type (custom_command) running admin-configured local commands for TXT record create/delete, with record details passed through environment variables (DOMAIN, RECORD_NAME, RECORD_VALUE, TTL, ACTION). This is the escape hatch for DNS providers not natively supported (#249 — e.g. nicmanager or any of lego's 220 drivers reached through a small wrapper script); lego itself ships no record-level CLI, so a direct binary call was not possible. Hardened: absolute binary path required, no shell (argv-split, no pipes), hard timeout (5–300 s, default 60), output truncated in error reports. The command binaries must exist and be executable or the provider test reports it.
  • dns-persist-01 ACME challenge support (draft-ietf-acme-dns-persist-01) — the built-in ACME server can now authorize DNS identifiers via a persistent TXT record at _validation-persist.<fqdn> bound to the requester's ACME account (accounturi) and an issuer-domain-name, so clients renew without writing DNS records. Opt-in under ACME settings, off by default: persistent validation ties issuance capability to the account key for the record's lifetime (draft §7.2), and the UI states this on toggle. policy=wildcard is honored for wildcard/subdomain scope (draft §5/§6), persistUntil rejects new validations past its timestamp, malformed records produce ACME malformed errors, account mismatches unauthorized. Issuer-domain-names come from the configured CAA identifiers, falling back to the ACME public hostname. Challenges still expire per the normal ACME authorization lifecycle.
  • acme.dns01_nameservers accepts host:port entries — the optional authoritative-resolver override (used by DNS-01 and dns-persist-01 validation) can now point at resolvers not listening on port 53 (e.g. a loopback-only BIND or a dnsmasq instance on an alternate port). Comma-separated as before; plain IPs still work.
  • Certificates issued from a template now record how they diverged from it — key type, validity and digest are template defaults that a request may legitimately override (issue #226 shipped that behavior); the issued certificate keeps its template link and now records which of those values were explicitly overridden (#258, option 2 debated there). The certificate detail shows the template name with a "modified from template" indicator listing the divergent fields, and the certificate list gains a Template filter to find diverged certificates. The record is written at issuance and frozen — editing the template later does not rewrite history. The value is exposed as template_overrides (list of field names) in the certificate API responses; renewals at par keep it.

Fixed

  • mTLS reverse-proxy certificate headers never honored — the mTLS middleware, login_mtls(), /api/v2/auth/methods, and /api/v2/mtls/enroll all matched X-SSL-Client-* names against dict(request.headers), whose keys WSGI reconstructs title-cased (X-Ssl-Client-Verify), so the exact-case in checks never matched and client-certificate authentication through nginx/apache proxy headers silently never fired (the designed spoof-attempt warnings were equally dead). All four sites now use request.headers directly, which is case-insensitive.

Full history: CHANGELOG.md


Installation

Docker (Recommended)

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

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

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

Debian/Ubuntu

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

Fedora/RHEL

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

Silent/Automated Install

# Skip firewall prompts for CI/automation
sudo UCM_PORT=8443 UCM_FIREWALL=no dpkg -i ucm_2.207_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.