github fabriziosalmi/certmate v2.48.3
v2.48.3 (what a key restricted to domains can reach, and pip 26.2.1)

4 hours ago

v2.48.3 (what a key restricted to domains can reach, and pip 26.2.1)

A patch release about API keys restricted with allowed_domains. Such a key no longer reaches what belongs to the instance and is filed under no domain: client certificates, backups, the inventory configuration. On /metrics it gets the series about its own domains only. An admin key restricted to domains, which can no longer be created, acts as an operator within its domains if one is still stored. A test now tries every route with restricted keys. A request is refused for who sent it before it is answered about its body. Two fixes in the create form: the Sectigo prevalidated challenge is offered when Sectigo is the default CA, and the form's panel is out of reach of the keyboard and of assistive technology while it is closed. The image and CI use pip 26.2.1. The API contract moves to 2.43: no field changes shape, and several answers change for requests that should not have been served (see "The API contract" below).


Read this before upgrading

If you use API keys with allowed_domains

allowed_domains restricts a key to domains. Some things belong to the instance and cannot be matched against a domain. A key that carries allowed_domains now gets 403 DOMAIN_OUT_OF_SCOPE on the routes for them:

  • client certificates: list, statistics, detail, download, create, batch, renew, revoke, CA reset. The CA certificate, the CRL and OCSP stay public, as before;
  • backups: every /api/backups route, including the list;
  • the inventory configuration: GET and POST /api/inventory/config.

Use a key without allowed_domains for those. Sessions and unrestricted keys are not affected. Each refusal is in the audit log.

Two answers also change for such a key on certificate routes:

  • Reissue checks the key's scope before it looks for the certificate. For a name outside the scope it answers 403 whether or not a certificate exists.
  • A certificate whose names cannot be read is refused. The names a certificate covers come from the certificate and from the san_domains recorded with it. When the served copy is missing or damaged they are read from certbot's lineage (live/, then the newest generation in archive/). When certificate material is there and nothing names what it covers, a restricted key gets 403 for it. An issuance in progress is not that case: the key that asked for it can follow it.

If you scrape /metrics with a key that has allowed_domains

/metrics now answers such a key with the series about its own domains only: the ones with a domain label its scope covers (expiry, last and next renewal, requests, renewals, ACME errors), under the same rule as the certificate routes. It no longer gets the instance's totals, the per-provider and per-status counts, the queue depths, uptime or version.

The Grafana dashboard and the alert rules in monitoring/ read those totals. If you use them, scrape with a viewer key that has no allowed_domains. A restricted key is the view for a team that monitors its own certificates.

If you have an admin key with allowed_domains

Creating one has been refused for some time, because admin reaches settings, users, keys, DNS accounts, storage and backups, and none of those can be restricted to a domain. A key created before that refusal was still honoured as admin.

It now acts as an operator within its domains: the restriction is the part that is kept. It no longer reaches admin routes. GET /api/keys and Settings show it as operator, and at startup the instance names such keys in its log (by key name, never by token):

N API key(s) are stored with the admin role and allowed_domains. ... each acts as an operator within its domains.

Replace each with an operator key (restricted) or an admin key (unrestricted), whichever you meant.

If a client of yours reads the status code of a refused request

A request with no credentials is now answered 401, and one below the route's role 403 INSUFFICIENT_ROLE, before its body is validated. Seven routes that validate their body used to answer such a request 400 about the body: create certificate, browser deployment report, create and restore backup, storage test and migrate, test CA provider. With the role, the body is validated as before.

If you send browser deployment reports yourself

POST /api/certificates/deployment-status/browser keeps method and source only as a lower-case label of at most 32 characters (letters, digits, hyphens) and checked_at only as an ISO 8601 date-time. A value in another form is replaced by the default (browser-fallback, browser, the server's time). The dashboard already sends that form.

The API contract

The contract version moves from 2.42 to 2.43. Nothing is added or removed and no field changes shape. What changes is the status code, or the content, of answers to requests that the documented rules already said should not be served: a key restricted to domains on something outside them, an admin key that was restricted, a request without the role. By this project's rule a changed status code is a major change; these are counted as the security fixes they are, as 2.31 and 2.32 were. One of them shipped in v2.48.2 with the number unmoved, and that release's notes now say so.

If you scan the image

The image's pip goes from 26.1.2 to 26.2.1, which fixes CVE-2026-13346 in pip. A scanner will report more findings for this image than for 2.48.2 (Trivy: 258 against 254), and that is the number catching up with the contents:

  • pip 26.1.2 and 26.2.1 vendor the same msgpack 1.1.2 and setuptools 70.3.0, byte for byte. 26.2.1 adds a CycloneDX list of what it vendors (pip/_vendor/bom.cdx.json), and scanners read it. The findings in those two packages were in 2.48.2 as well, unreported.
  • 26.2.1 also moves the vendored urllib3, idna and pygments forward. Counted with OSV on each vendored version, known vulnerabilities in pip and its vendor tree go from 11 to 6.

pip is not used when CertMate runs; it runs when the image is built.


Fixed

  • Client-certificate routes refuse a key restricted to domains (#1166), above.
  • Backups and the inventory configuration refuse a key restricted to domains; reissue checks scope first (#1167), above.
  • A certificate whose names cannot be read is refused to a restricted key (#1163), above.
  • An admin key restricted to domains acts as an operator (#1168), above.
  • The role is checked before the body is validated (#1172), above.
  • A browser deployment report is kept in the form the dashboard sends it (#1173), above.
  • /metrics gives a key restricted to domains the series about its own domains (#1168, #1171), above.
  • The Sectigo prevalidated challenge is offered when Sectigo is the global default CA (#1170, by @QuentinBtd). The create form offered it only when Sectigo was selected by hand; with the CA left on "Global default" it was hidden, although the server accepts it. Checked in a browser against the previous build: offered, selectable and issued through the Sectigo account; still hidden and cleared when another CA is chosen.
  • The create panel is out of reach while it is closed (#1176). The panel stays in the page when it is closed, and Tab still stopped on each of its controls, none of them on screen; assistive technology was offered a dialog named "New certificate" that was not open; and pressing Escape straight after opening sent focus into the closed panel. The panel is now inert whenever it is closed. Measured in Chromium with real keys: 13 of the dashboard's 40 Tab stops were inside the closed panel, now none. Opening it by keyboard, by pointer, from the command palette and from "Edit & reissue" is unchanged.

Changed

  • pip 26.2.1 in the image and in CI (#1165), above. requirements-build.lock is regenerated; only pip's entry and hashes change.

Tests

  • Every call of the route walk is tried first with keys restricted to a domain that owns nothing (#1168): a viewer, an operator, and a key stored as admin with allowed_domains. Then every route the walk does not have is tried. The test fails if such a key gets a name it did not send, certificate or key material, a download, a change made, or anything but a refusal on a route about a domain. A route added to the application has to be added to the walk, so it is tried here without anyone remembering to. The dashboard's own routes that take a body (create, batch, batch download) are outside the walk and get valid requests of their own from the same keys.
  • Every call of the route walk is also tried with no credentials, as a viewer and as an operator (#1175). What each of the three reaches is compared, in both directions, with a list written down in the test: a new route fails it until someone has written who may reach it, and a route that stops answering a role fails it too. On every answer it also checks that a private key is answered only where listed and that no secret the owner sent during the walk comes back.
  • The scope tests keep to their own minute and to the loopback (#1169): they no longer share the rate limit or reach the network.
  • The scan's time budget is counted in CPU time (#1178). The test that keeps the log scrubber linear on hostile input timed it on the clock, and failed this release's gate on a busy machine with the code unchanged. It now counts the CPU time of its own thread; slowed or made quadratic on purpose, the scan still fails it.

How it was checked

  • Restricted keys: the real application with real keys. Each of the nine client-certificate routes, the eight backup routes, and the two inventory-configuration routes, left unprotected one at a time, fails its test. Controls: a key without allowed_domains does what its role allows on the same routes, and the public routes answer with no credentials.
  • The admin key: stored the way an earlier release stored it, it gets 403 on admin routes that an unrestricted admin key gets 2xx on, downloads a private key inside its domains and is refused outside them.
  • The route walk with restricted keys: 846 attempts, 635 of them refused, none answered with something that was not the key's. With each part of this release removed one at a time (the role rule, the backup list, the /metrics view, the client-certificate download, the inventory configuration, the order of checks in reissue), the test reports it on its route.
  • /metrics: three certificates (in scope; another domain; filed in scope and covering a name outside it). The restricted key gets its domain's series, none about the other two, none without a domain label, and the answer parses as Prometheus text. A key without the restriction gets all of it.
  • Names from the lineage: a damaged served copy with the names only in live/; only archive/, generations 2 and 10; nothing readable; an issuance held in flight and read by the key that started it. Ten deliberate breakages, each caught.
  • Role before body: the routes are found the way the application finds them (a model and a role). No credentials is 401 for an empty body, a body that is not JSON and an unexpected one; too low a role is 403; with the role the body is still validated; a refusal has one audit entry.
  • Browser reports: the dashboard's own values are kept as sent; markup, a sentence, 33 characters, upper case, a trailing line break become the default; 600 KB of field values leave the certificate's metadata under 2 KB.
  • pip: the default, minimal and extras images build with 26.2.1 and answer certbot --version; pip freeze --all differs from 2.48.2's by the pip line only; Trivy compared per finding.
  • The release gate runs on the release commit: the suite, the browser tests, and real certificates from Let's Encrypt staging.

Not verified

  • The create panel in other browsers, and with a screen reader. Measured in Chromium only. "Offered to assistive technology" means present in the accessibility tree Chromium exposes; no screen reader was run. inert needs Chrome 102, Firefox 112 or Safari 15.5; an older browser ignores it and behaves as before.

Don't miss a new certmate release

NewReleases is sending notifications on new releases.