github fabriziosalmi/certmate v2.49.0
v2.49.0 (certificates CertMate did not issue: imported with a CA for their next renewal, or external and replaced by their owner)

4 hours ago

v2.49.0 (certificates CertMate did not issue: imported with a CA for their next renewal, or external and replaced by their owner)

A minor release. A certificate issued elsewhere can now be imported without issuing anything, either with a CA configured here for its next renewal (#1227), or as an external certificate that no CA here renews: CertMate stores, deploys and watches it, and its owner replaces it by upload (#1159). The CA account's own email now counts at every gate before issuance (#1228). An adopted certificate runs its deploy hooks once (#1237). The API contract moves from 2.44 to 2.48.


Read this before upgrading

Nothing needs to change on your side. No certificate CertMate issued renews differently, and no setting or file format changes. What a caller may notice:

A CA that is not configured is refused before anything starts

A create or a reissue naming a CA other than Let's Encrypt that is not configured on this instance used to be accepted and fail later; an async request answered 202 and its job failed. It now answers 400 with the CA's reason, and no job is queued (#1228). The dashboard's batch create says the CA or its account is not configured, without the text of the underlying error. Let's Encrypt is unchanged: with no saved configuration, certbot's defaults still apply, staging included.

The CA account's email counts at every gate

Issuance already used the selected CA account's email ahead of the global one. The checks before it required the global email even so, and refused a create, a reissue, a batch or an adoption with only an account email. They now resolve the email the way issuance does: the account's, then the global one, and a refusal only when neither is set (#1228).

If you call the API

By contract number:

  • 2.45 POST /api/certificates/import; certificate reads gain imported_at and ca_account_id, and renewal_info.status gains imported.
  • 2.46 external: true on the import; reads gain external, and renewal_info.status gains external; EXTERNAL_CERTIFICATE and NAMES_ALREADY_MANAGED (409).
  • 2.47 POST /api/certificates/<domain>/replace; reads gain replaced_at; an external certificate reads with renews_at null.
  • 2.48 a create, reissue, batch or import through a CA that names what an external certificate covers answers 409 NAMES_ALREADY_MANAGED.

Every change is in docs/api.md.


New

  • Import an existing server certificate (#1227, contributed by QuentinBtd). POST /api/certificates/import stores a certificate, its chain and its key, checked for dates, key match, chain signatures and constraints, without asking any CA. The request names the CA, account and challenge for its next renewal, and auto_renew decides whether the sweep renews it when due; the first renewal makes a normal certbot lineage, and the imported certificate keeps serving if it fails. In the dashboard: Import an existing certificate in the create form, with the identity of the certificate shown before anything is sent and the three files checked together.
  • External certificates (#1239, #1240, #1241, #1242; #1159). For a certificate from a CA CertMate cannot reach, a vendor portal or an internal CA without ACME, import it with external: true, or Renewal: Not by CertMate in the dashboard:
    • It is stored, listed, downloadable, deployed and watched for expiry like any other, and never issued, renewed or reissued. Every path that would ask a CA for it answers 409 EXTERNAL_CERTIFICATE, the renewal sweep counts it under external and leaves it alone, and its expiry warnings come as for a certificate with auto-renew off.
    • Its owner replaces it by upload, with POST /api/certificates/<domain>/replace or Replace certificate in its panel. The four files are published together as a renewal publishes them, deploy hooks run with CERTMATE_EVENT=renewed, and the probe, notes and tags are kept. An upload of the certificate already in place is refused, and so is one that expires earlier, unless the request says that is meant.
    • A name belongs to one certificate: an external import cannot take a name a managed certificate covers, and a create, reissue, batch, adoption or import through a CA cannot take one an external certificate covers. Two certificates CertMate issues may still share a name, as today.
    • "External" is recorded twice, in the certificate's metadata and in its settings entry, and either one is enough. A missing CA otherwise means "the default CA", so losing one record must not turn the certificate into one CertMate reissues.

Fixed

  • An adopted certificate ran its deploy hooks twice (#1237). Adoption announced the new certificate once from the issuance and once more from the adoption route; it is now announced once, with the adoption marked on the same event.
  • The MCP renew tool described itself as a forced renewal (#1238). It never sent force, so a certificate that was not due was not renewed. The description now says what the tool does, and the tool still does not force.
  • The CA account's email, and a CA that is not configured (#1228, contributed by QuentinBtd), as described above. An adoption plan for a CA that is not configured says so, and still reports the email and DNS provider that are configured.

How it was checked

  • The end-to-end bench with a private CA, rebuilt from main at 9cae3dd, with the scheduler's real renewal job:

    • an external certificate was imported, and the webhook receiver got certificate_created with external: true;
    • renew, force renew, reissue and turning auto-renew on answered 409 EXTERNAL_CERTIFICATE, and a create naming its SAN answered 409 NAMES_ALREADY_MANAGED before any issuance;
    • the real renewal job renewed the bench's 11 other certificates through the private CA and reported "1 external", leaving the external certificate's serial unchanged;
    • a replacement expiring earlier was refused; one expiring later was served, its certificate and key matching, and the receiver got certificate_renewed with replaced: true;
    • a new certificate was issued from the private CA over HTTP-01 beside it.

    Before #1227 was merged, two certificates from a vendor-style root were imported with the private CA as their renewal source: with auto_renew on the real job renewed one through that CA into a normal lineage, and with it off the job left the other alone until the switch was turned on.

  • The dashboard in a browser: the browser suite against an image built from #1241 (142 passed, 3 skipped, 1 expected failure), with the new tests for the external import form, the row, the panel and the replacement.

  • Deliberate breakages: 21 in the external-certificate changes (the import lock, each record of the marker, each refusal of the replacement, each part of the name rule, what the dashboard sends); each was caught by the intended test.

  • The suite (8,805 tests) on main at 9cae3dd, and the coverage floors of the HTTP layer. The release gate runs the suite, the browser tests and real certificates from Let's Encrypt staging on the release commit.

Not verified

  • A certificate from a public vendor's CA as an external certificate. The bench and the tests use chains made for the purpose; no certificate bought from a vendor was imported.
  • A real Sectigo or other paid account for the import's first renewal and for #1228's email rule. Both are covered with the CA mocked, and the renewal with the private CA.
  • Azure Key Vault. The external marker and replaced_at survive its tags in tests; no live vault was used.
  • An external import and a create of the same name started at the same moment. Both can pass; the window is documented, and #854's name reservation is what closes it.

Don't miss a new certmate release

NewReleases is sending notifications on new releases.