github fabriziosalmi/certmate v2.33.0
v2.33.0 (The client CA is yours, and four answers stop being reassuring)

6 hours ago

v2.33.0 (The client CA is yours, and four answers stop being reassuring)

A minor release: one new capability, four fixes to answers the API was giving
with more confidence than it had, and the gates that will catch the next one.

The API contract moves to 2.3. Read it from X-CertMate-API-Version on any
response, or from api_contract_version in /api/health.

The client certificate authority is yours, and it can be rebuilt

The private CA that signs client certificates was generated once, on first
start, with a subject nobody chose. It is now client_ca_subject in settings,
validated before anything is generated rather than discovered afterwards: a
country that is not two letters is refused, empty fields are omitted instead of
being written as blanks, and the common name falls back rather than being left
out.

POST /api/client-certs/ca/reset rebuilds it. The old CA is put aside and the
client certificates it signed are deleted, because a certificate signed by a CA
that no longer exists is not a certificate anyone can verify. Server
certificates, settings and DNS accounts are not touched. The confirmation is a
typed string rather than a boolean, so the request cannot be made by accident,
and the reset is written to the audit log.

This is the new endpoint that moves the contract to 2.3.

A refusal answered like an empty result

GET /api/certificates without a credential answered 401 with a certificate
object whose every field was null, and no error, no code. A client reading
the body rather than the status could not tell "you are not allowed to look"
from "there is nothing there".

The cause was decorator order, not a missing branch. flask-restx's
marshal_with reshapes whatever the view returns, so with the marshaller
outside require_role the rejection was marshalled into the success model:
error and code are not declared there, so they were dropped, and every
declared field became null. Four of seven marshalled resources had it that way,
/api/certificates, /api/backups, /api/cache/stats and /api/cache/clear.

docs/api.md has always said that every failure carries an error and a
code. These four were violating the published format, so nothing about the
contract changed; they now follow it. /api/auth/me still answers
{"user": null} with 401, which is its own documented shape for a UI deciding
whether to draw a login form.

A health check said "ok" when it could not look

check_storage exists to answer one question: are the certificates really in
Azure, Vault or S3, or did the backend fail to initialise and CertMate fall
back to local disk. When it could not read the backend at all, the exception
was swallowed and the check fell through to the healthy answer, logging
nothing. An instance that could not look reported the same thing as one that
had looked and found everything in order.

The state is now unknown and the failure is logged. The severity deliberately
does not move: inventing a degraded status out of a question that could not be
asked would page someone for nothing, and /health keeps state and severity
apart precisely so a check can be honest in the detail without moving the
aggregate.

"Not there" and "could not tell" were the same answer

Every remote storage backend answered certificate_exists() with False for any
exception, so a timeout, a 403 and an expired credential all read as "the
certificate is not there". That is the shape that makes a present certificate
get re-issued.

The contract is now three-valued: True is present, False is definitely absent,
and CertificateExistenceUnknown says the backend could not tell and which
error stopped it. Each backend narrows to the absent signal its own SDK raises,
matched by class name and error code rather than by importing an optional
dependency. Infisical answers by listing instead, because its library cannot be
installed here and guessing its not-found exception would have been an invented
contract rather than a copied one.

Verified against real MinIO and Vault: a wrong password and a wrong token now
raise, where before both answered "not there".

Also

  • The bearer-token file reader decided in silence. An unreadable
    API_BEARER_TOKEN_FILE correctly declines to reconcile, and it cannot open
    an instance, but an operator who had rotated the token in that file got 401
    on every request with nothing anywhere saying the file could not be opened.
    It says so now.
  • /api/cache/stats and /api/cache/clear were bound twice, and the binding
    in the web layer was dead. Not harmless while it lasted: the live one writes
    an audit entry and the dead one did not, so a change in registration order
    would have silently stopped auditing cache clears.
  • Thirty-three public endpoints were documented in nothing at all, /api/health
    among them. They are in docs/api.md now.
  • The published contract at /api/swagger.json was missing five fields the code
    returns, and described usable as the defect that had just been corrected.
  • The SDK and CLI called a valid certificate expired for the same truncation
    reason the dashboard did. Fixed in clients 0.1.5, already on PyPI.

Where the release notes live

This page is docs/releases/v2.33.0.md. Until this release every version's
notes were sections of one 4,780-line RELEASE_NOTES.md; each release now owns
a file, and RELEASE_NOTES.md is the index, generated from that directory.

Existing links still work: every ## vX.Y.Z (title) heading is still in the
index, so an anchor into it resolves as it did.

The reason is not the size. One file held both an append-only archive of things
that were true once and a page the gates read as a description of the product
now, so five of them had to exclude it by name to avoid reading a 2024 pin as a
current claim. One of those five exclusions named a CHANGELOG.md that has
never existed here. There is now one definition of "this records the past", and
it covers the directory.

The gates that would have caught these

Four of the fixes above existed because something checked one direction and not
the other, so the gates are part of the release rather than a footnote.

  • The contract now moves with the surface. POST /api/client-certs/ca/reset
    was itself about to ship without bumping the contract version, found while
    cutting this release. A snapshot records the surface and the version
    together, and changing one without the other fails.
  • Every endpoint that exists must be written down. The existing gate asked
    whether every documented endpoint exists; nothing asked the reverse, and the
    answer was 49 routes out of 102.
  • An exception budget instead of a number that only grows. No bare
    except:, no handler whose body is only pass, and two pins that move only
    downwards: the total, and the handlers that neither record the failure nor
    say why silence is right.
  • A translation records the English it was made from. Twenty-four pages
    were behind, including four describing an API contract that had changed in a
    breaking way. Each page now carries the hash of its source, so nothing about
    editing the translation can hide that it is stale.

Don't miss a new certmate release

NewReleases is sending notifications on new releases.