github fabriziosalmi/certmate v2.48.0
v2.48.0 (when a certificate renews, which kind it is, and one form for every date)

3 hours ago

v2.48.0 (when a certificate renews, which kind it is, and one form for every date)

A minor release. One rule now decides when a certificate renews, CertMate makes the decision and certbot carries it out, and the API says when that will be (#393). A certificate can ask Let's Encrypt for a profile, tlsserver (45 days) or shortlived (160 hours), from the API or the dashboard (#395). Every date-time in an API answer is ISO 8601 in UTC with a Z (#1127). Audit records made through the API or the dashboard say who acted instead of "system" (#1136). And the API reference is now compared with the answers the routes give, which corrected the client-certificate examples throughout (#1105). The API contract moves from 2.39 to 2.42.


Read this before upgrading

When a certificate renews

For a 90-day certificate with the default 30-day threshold, which is every Let's Encrypt certificate CertMate issued before this release, renewals happen when they did. What changes:

  • One rule decides, and the answer carries it. A certificate renews at the threshold where that is at most half its lifetime, otherwise at a third of its lifetime (half under 10 days), or at the point its CA's renewal window (ARI) names, earlier or later, but never with less than a sixth of the lifetime left and never after the window's end. GET /api/certificates/<domain> and the list carry the instant as renews_at, and needs_renewal is true from it. Before, needs_renewal was days_left <= threshold, which said "due" up to a night before anything happened for a 90-day certificate and two weeks before for a 45-day one, while certbot's own gate decided.
  • certbot is told to renew. Every renewal CertMate decides by time is run with --force-renewal, so certbot's own gate (two thirds of the lifetime since certbot 5.x) no longer takes part, and a certbot upgrade cannot change when CertMate renews. A certificate due only because its served key is missing or does not match is still repaired from its lineage without a new key, as before.
  • The CA's window can postpone a renewal now, not only bring it forward. For a 90-day Let's Encrypt certificate the window sits around 30 days before expiry; a postponement stops at 15 days before expiry. The sweep summary counts these as ari_postponed.
  • A threshold above half the lifetime no longer counts for that certificate: the lifetime rule decides. A 30-day threshold on a certificate that lives 31 days used to call it due the day it was issued.
  • Expiry warnings follow the renewal. With auto-renew on, a warning speaks once half of the renewal margin has passed without a renewal: the same marks as always for a 90-day certificate, the last two for a 160-hour one, never one on the day it is issued.

Date-times in API answers

Every date-time is ISO 8601 UTC with a Z: 2026-10-02T22:26:14.669591Z. Before, 39 fields had no offset at all (2026-10-02T22:26:14.669591, which datetime.fromisoformat() and new Date() read as local time) and 5 had +00:00, sometimes in the same answer. The instants did not change, only how they are written. If your code appended a Z itself, stop; if it compared these strings with stored ones, compare instants. Python 3.11 and later parse the Z; on 3.9 and 3.10, replace it with +00:00 before fromisoformat().

One field keeps its own form: a certificate's expiry_date (2026-11-30 09:17:11, UTC), which clients parse today. It is deprecated; expires_at, new, is the same instant in the form above.

The audit trail

63 places that write an audit record passed no actor, and every record they wrote said "actor": {"kind": "system"}, with the person or token only in user: deleting a certificate, creating or revoking an API key, creating or deleting a user, restoring a backup, changing the authentication configuration. Made inside an API or dashboard request, those records now carry the identity that made it (api_token, user, or agent) and its trigger; outside a request (the scheduler) they are still system. A SIEM rule that keys on actor.kind == "system" will see far fewer such records. The client CA reset recorded a username as its actor, the only record with a string there; it is {kind, label} like the rest.

Deprecated, removed in 3.0

Still sent until then; listed in #1114:

  • a certificate's expiry_date: read expires_at;
  • paths in the answers of POST /api/client-certs/create and /renew: server-side file paths a client cannot open; the downloads are the way to the files;
  • a client certificate's crl_entry_serial: nothing has ever set it; the CRL lists a revoked certificate by its serial_number.

If you call the API

By contract number: 2.40 renews_at, and needs_renewal decided by the rule above; 2.41 acme_profile on create and reissue, and acme_profile, acme_profile_withdrawn_at on the certificate; 2.42 one form for date-times, and expires_at.


New

  • ACME profiles (#395). acme_profile on POST /api/certificates/create and POST /api/certificates/<domain>/reissue: tlsserver (45 days, no Common Name), shortlived (160 hours) or classic on Let's Encrypt. A CA account can name a default. The profile is required at issuance (a CA that does not offer it refuses the order) and preferred at renewal: a CA can withdraw a profile (Let's Encrypt retired tlsclient in July 2026), and a renewal that failed on it would let the certificate expire, so the renewal asks for it and, if the CA no longer lists it, gets the CA's default, logs a warning and records acme_profile_withdrawn_at. In the dashboard: ACME Profile in the create form's advanced options (Edit & Reissue opens on the certificate's own) and Default ACME profile in the CA account form.
  • When a certificate renews, in the certificate's panel (renews_at), with its ACME profile.

Fixed

  • The dashboard showed every date-time shifted by the viewer's UTC offset: it reads them with new Date(), which takes a value without an offset as local time. Two hours, in Italy in summer.
  • A backup listed without its metadata showed its creation time in the host's local time, not UTC.
  • Audit records made through the API or the dashboard said "system" (above).
  • The API reference (docs/api.md and its four translations): the client-certificate examples showed fields no answer sends (status, revocation, renewal, a flat serial_number on create) and left out most of the ones it does; the certificate example lacked auto_renew; the audit verify example lacked its checkpoint fields; GET /api/web/update-check was described as session-only and takes the API token.

How it was checked

  • Real certificates, Let's Encrypt staging through Cloudflare DNS-01: 29 of 29 against an image built from bbf9fd1, which differs from the release only by these notes and the version bump, in 10 minutes. scripts/release.sh prepare runs them again before the release is cut. Among them, the new profile tests: a tlsserver certificate is 45 days with no Common Name, the renewal configuration certbot itself wrote is turned from required to preferred, a forced renewal stays tlsserver, a shortlived one is 160 hours and renews about 3.3 days after issue, an unknown profile is refused; and a renewal driven by staging's own ARI window.
  • Every route of the API, called on a sealed instance in CI (the route walk): now also the three dashboard routes the reference documents. New rules there: no field is sent with two types (it found the CA reset's string actor), every date-time in every answer has the one form, no record a token makes reads "system".
  • The API reference against the answers: every code block in the five api.md files says what it is, and every response example is compared field by field with what the route sends. What remains out of line is 5 blocks, all parts the sealed instance cannot fill (CAA records, RDAP registrations, domain health, a refusal by scope, the update check, which is off there).
  • The renewal rule: a table of lifetimes (160 hours, 45, 90, 180 days) with and without a window, the floor, the window's end, and the instant shown being the instant acted on.
  • Deliberate breakages: 11 of the renewal rule, 14 of the profiles (backend and dashboard), 5 of the date-time form, 3 of the walk's new coverage, 1 of the audit attribution; each was caught by the intended test.
  • The suite (8,123 tests) and the dashboard's browser tests (96), on the last change before the release; prepare runs them again.

Don't miss a new certmate release

NewReleases is sending notifications on new releases.