github fabriziosalmi/certmate v2.30.0
v2.30.0 (Three reported bugs, credentials that can live elsewhere, and a build that is the same twice)

3 hours ago

v2.30.0 (Three reported bugs, credentials that can live elsewhere, and a build that is the same twice)

A minor release, and a large one: 35 changes. Three of them come from bug
reports, several close gaps that no test had ever failed on, and two change how
the image is built.

The three reported bugs

A certificate that never auto-renewed (#759). Reported against a private CA
with a 31-day lifespan and a 30-day threshold. Neither was the cause: a
registered certificate with those numbers renews from the day it is issued.
What happened is that the renewal sweep iterates settings['domains'] and
nothing else, while the dashboard and the digest both answer "which certificates
exist" as the union of settings and the certificate directories. A certificate
present on disk and absent from settings was therefore visible everywhere except
in the component whose job is to renew it - and that component said nothing at
all, which is why the report reads as a threshold bug. The sweep now names every
such certificate, says what to do about it, and counts it. Whether it should
also renew them is being decided in #792.

"Failed to update metadata for domain" (#757). One message for two unrelated
causes: a metadata file that could not be written, and a write refused by design
because the file on disk was written by a newer build. Both were computed,
logged, and thrown away before reaching the person who had to act on one of
them. A write that cannot happen now says why, and the deliberate refusal is a
409 with the override that lifts it, not a 500.

The snooze menu was cut off (#758). It reads as a z-index problem and is not
one: no z-index escapes an ancestor with overflow: hidden. Measured in a
browser, 78 of the menu's 94 pixels were clipped and none of the three options
could be clicked.

Credentials can name where they live

DNS provider API tokens and the OIDC client secret had to sit in settings.json
as cleartext JSON - the file that gets backed up, copied between hosts and
mounted into a container. Any of those fields can now name a file or an
environment variable instead:

"prod": { "api_token_file": "/run/secrets/cloudflare_api_token" }
"oidc": { "client_secret_env": "CERTMATE_OIDC_CLIENT_SECRET" }

The value is read where it is used and never written back, so a settings save
does not turn a reference into a stored secret. A reference beats a stale
literal, trailing whitespace is stripped (both docker secret and
kubectl create secret --from-file leave a newline), and a reference that
resolves to nothing is refused rather than treated as empty. Documented in
SECURITY.md and in the Docker guide in all five languages.

The image installs the same packages twice running

requirements.txt pins 42 packages and resolves to 118. The other 76 were
whatever the index served on the day the image was built, with no record of
which. requirements.lock and requirements-minimal.lock now hold the full
resolution, generated by pip's own resolver inside the pinned base image, and
the build installs from them. Both published architectures resolve the set
identically - measured, and re-checked whenever the lock is regenerated.

If you change a pin, regenerate the lock with scripts/regenerate_lockfiles.sh.
A check fails when the two disagree, because otherwise a merged security bump
would silently not ship.

Things that were quietly wrong

  • An unreadable credential file left the instance open instead of locked.
  • An unreadable secret key was treated as permission to invent a new one, which
    signs out every user on every restart with a key nobody chose.
  • Three ways the instance could start without knowing its own state.
  • An interrupted deploy left no record at all, so it looked like it never ran.
  • A backup taken without a passphrase could not restore the instance it came
    from, and said nothing about it.
  • The event stream did not go through the same authentication gate as everything
    else.
  • Two writes to a domain's DNS provider could interleave, leaving the two files
    that record it disagreeing.
  • What counts as a secret when masking was decided by a regular expression that
    could miss a provider's credential field. Every one is now covered or
    declared, and a test walks the registry.

For anyone writing a client

  • Every response carries X-CertMate-API-Version, which moves when the
    interface moves and not when the release number does. There is a deprecation
    mechanism (RFC 9745 / RFC 8594 headers) shipped before anything needs it.
  • Certificate and download failures carry a machine-readable code.
  • The request models the API publishes are now enforced rather than advertised.
  • Every unit of work has a correlation id, including the background ones.

Faster, and honest about it

A dashboard load against a remote storage backend was N sequential network round
trips; it is now bounded concurrency, and the cert-info cache serves the second
load with none. Event dispatch is a bounded pool instead of a thread per event,
and a backlog is visible. Three operations that had a budget and no measurement
now have recorded numbers.

Also

The renewal sweep says what it did and whether it finished. A renewal that did
not reach production says so, once. Every environment variable the code reads is
documented, and a gate keeps it that way. The health endpoint went from the most
complex unit in the repository to one function per check. The dependency scan
now looks at what actually ships rather than at the manifests.

Don't miss a new certmate release

NewReleases is sending notifications on new releases.