Skip to content

feat: set how many days before expiry a certificate becomes a finding - #110

Merged
Coysh merged 3 commits into
mainfrom
feat/certificate-warning-window
Oct 21, 2026
Merged

Coysh merged 3 commits into
mainfrom
feat/certificate-warning-window

Conversation

@Coysh

@Coysh Coysh commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

What changed

The warning window is now a setting. Until now a certificate became a finding 30 days before it expired, always. A Let's Encrypt certificate renews itself around 30 days out, so it was flagged on every renewal cycle until the renewal landed, and the finding taught people to acknowledge it unread.

  • Settings → General has a "Warn about expiring certificates" form (owners only, recent authentication required): a whole number of days, 1 to 90. Empty means "follow the installation default" and is stored as null, so an organisation that never chose keeps following it.
  • Default comes from manager.findings.certificate_warn_days, set by MANAGER_CERTIFICATE_WARN_DAYS. Read with an is_numeric guard and clamped to 1-90, so a blank line in .env means 30 rather than zero.
  • Urgent (high) is inside 7 days or the whole window, whichever is smaller.
  • The rule, manager:certificates:check output and the Security tab's Expires row all ask Organisation::certificateWarnDays(), so they cannot disagree.
  • Saving writes an organisation.certificate_warn_days.updated audit event with the before and after values. The hourly sweep applies a new window to every site.
  • One migration: nullable organisations.certificate_warn_days (unsigned smallint). Reversible.

An acknowledgement belongs to one certificate. A certificate short-lived enough to stay inside the window never resolved, so each new one inherited the previous one's acknowledgement. There is a new optional ResetsAcknowledgement interface; CertificateExpiring implements it by comparing expires_at. When the finding stays outstanding but the expiry has moved, the acknowledgement is cleared and a finding.reopened audit event is written (before: the old reason; after: state, title and a plain-language reason). No notification is sent, since the finding never closed. No other rule is affected.

Needs no new contract and no hosting-layer binding: it is an organisation column plus a config value, so EditionSeamTest and NoCloudCodeTest are untouched and green.

Docs: docs/env.md, docs/monitoring.md, .env.example, CHANGELOG 1.9.0, and the version examples in getting-started, upgrade and rollback.

Verification

  • Invariants suite: 643 passed
  • Full suite: 1507 passed
  • Pint, PHPStan: clean
  • npm run build produces no change to public/build
  • Seeded site with 20 days left and a 14-day window: sweep opens no finding; with the default window it opens one; back to 14 it resolves.

Coysh added 3 commits October 21, 2026 07:47
A certificate that renews itself thirty days before it ends is inside a thirty-day window for part
of every cycle, so the finding opened each time and taught people to acknowledge it unread. The
window is now an organisation setting, one to ninety days, defaulting to the installation's value
(thirty unless MANAGER_CERTIFICATE_WARN_DAYS says otherwise). The urgent tier is seven days or the
whole window, whichever is smaller. The rule, the daily check's output and the Security tab all
read it from one place.

An acknowledgement could also outlive the certificate it was about. A certificate short-lived enough
to stay inside the window never resolved, so each new one inherited the previous one's
acknowledgement. A rule can now say its subject changed, and the certificate rule does when the
expiry moves; the finding returns to open, the audit log records finding.reopened with the old
reason, and no notification is sent for it.
The default-window test set the variable with putenv() alone. env() reads
$_ENV and $_SERVER before the process environment, and a .env copied from
.env.example carries MANAGER_CERTIFICATE_WARN_DAYS as a blank line in both,
so on any checkout set up that way - CI copies the example - the three
cases that choose a value read the blank and got 30.

The test now sets and restores all three, so it holds whether or not the
checkout's .env already has the key.
# Conflicts:
#	CHANGELOG.md
#	docs/rollback.md
@Coysh
Coysh merged commit 17c3fac into main Oct 21, 2026
3 checks passed
@Coysh
Coysh deleted the feat/certificate-warning-window branch October 21, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant