Evidence
What the green checks hid
For twelve days a team wiki's backup reported success while it saved none of the files the team had uploaded. Every check was green. This page is three of those moments, what was actually true, a restore measured on a machine that had never run the stack, and the system that catches the difference, including the mistakes of the AI agent that writes most of this code.
- repositories under one standard
- 91
- restore scripts, all run by CI
- 73of 73
- clean-machine restores measured
- 45
- releases tagged
- 1231
Counted by fleet-ops, which rewrites the numbers when one changes. Last changed 2026-10-13 20:26 UTC.
Case one · a backup of nothing
12 days, 8 releases, and not one attachment in any backup
Data backup OK CI: data archive opens ✓
Release 2.0.0 moved Outline's file storage from MinIO to Garage. The backup kept archiving the old MinIO volume, which nothing writes to any more. The restore script looked for a service that no longer existed and would have stopped before doing anything.
A team restoring after a lost disk gets its documents back with every image and attachment missing. Every log line in those twelve days would have said OK.
An archive that opens proves the archive. CI now proves the backup the way a user would notice it missing: it stores a file through S3, takes a backup, stores a second file, restores, and requires the first to read back and the second to be gone. The restore uses Garage's own consistent metadata snapshot rather than a copy of a live database.
Case two · the restore nobody ran
72 restore scripts, some never run by CI since May 2021
test_restore_roundtrip PASS (every push, 47 templates)
Each test restored with its own copy of the commands. The scripts a person would run were never executed. Four were written for a different vendor's image, and one of those refused to run at all. One signed in with the wrong account. Several merged the backup into live data instead of replacing it.
The day you need a restore would have been the first day the script ran. In 16 of the 47 templates that script was more than three years old.
A decade of production infrastructure taught me one pattern above the rest: a backup that has never been restored is a belief. The tests now run the shipped script, and a fleet rule fails any repository whose restore script no test invokes. A comment or an echo naming the script does not count. Its list of exceptions can only shrink, and it has been empty since the day it was written.
Case three · the check that could not fail
17 hours healthy over a crashed server
docker inspect → healthy
A game server had crashed and dropped two players mid-round. The health check searched for the process with pgrep -f, which matches its own command line. It found itself, every time.
Time to detect was never. Docker does not restart an unhealthy container on its own, and this one was never reported unhealthy.
The rule it produced: a check whose pattern can match the check itself has never failed, so prove the red case against a real container before believing the green one. The fix shipped to every game-server template with that test, and every fleet rule since has been tested against a planted violation.
The question a CISO asks first
The host is gone. How long until it works again?
Every week, for each template below, one runner starts the previous release, writes a marker row into the database and a marker file into the data, and takes a backup. Only what an operator keeps off the host leaves it: the backup files and the environment file. A second runner that has never run the stack then brings the current release back from those files alone, with the scripts the template ships, and the clock stops when the application answers over HTTPS with both markers back. So the number below is an older backup restored into a newer release, on an empty machine, image downloads included.
Fastest 17 s · slowest 6 min 12 s
| Template | Backup taken on → restored into | Empty machine to answering | Of that, restoring | Backup every | Last run |
|---|---|---|---|---|---|
| AFFiNE | v1.9.3 → v1.9.4 | 1 min 1 s | 22 s | 24 h | 2026-10-11 |
| Audiobookshelf | v1.1.4 → v1.1.5 | 35 s | 11 s | 24 h | 2026-10-11 |
| Authelia | v1.8.3 → v1.8.4 | 37 s | 11 s | 24 h | 2026-10-11 |
| Beszel | v1.1.3 → v1.1.4 | 32 s | 11 s | 24 h | 2026-10-11 |
| Bitbucket | v1.9.4 → v1.9.5 | 1 min 3 s | 24 s | 24 h | 2026-10-11 |
| Confluence | v1.9.6 → main | 1 min 41 s | 43 s | 24 h | 2026-10-11 |
| Dify | v1.1.1 → v1.1.2 | 5 min 3 s | 2 min 48 s | 24 h | 2026-10-11 |
| Diun | v1.0.3 → v1.0.5 | 24 s | 10 s | 24 h | 2026-10-11 |
| Docmost | v1.9.3 → v1.9.4 | 1 min 25 s | 28 s | 24 h | 2026-10-11 |
| Dozzle | v2.1.6 → v2.1.7 | 30 s | 11 s | 24 h | 2026-10-11 |
| Forgejo | v1.1.2 → v1.1.3 | 40 s | 12 s | 24 h | 2026-10-11 |
| Gaseous | v1.3.2 → v1.3.3 | 1 min 43 s | 1 min 3 s | 24 h | 2026-10-11 |
| Ghost | v1.9.4 → v1.9.5 | 1 min 37 s | 25 s | 24 h | 2026-10-11 |
| Gitea | v2.0.3 → v2.0.4 | 48 s | 22 s | 24 h | 2026-10-11 |
| GLPI | v1.9.2 → v1.9.3 | 1 min 21 s | 42 s | 24 h | 2026-10-11 |
| Grafana | v1.8.2 → v1.8.3 | 1 min 47 s | 58 s | 24 h | 2026-10-11 |
| Home Assistant | v1.7.3 → v1.7.4 | 1 min 20 s | 15 s | 24 h | 2026-10-11 |
| Homebox | v1.7.1 → v1.7.2 | 31 s | 10 s | 24 h | 2026-10-11 |
| Homepage | v1.1.2 → v1.1.3 | 34 s | 11 s | 24 h | 2026-10-11 |
| Immich | v2.1.3 → v2.1.4 | 1 min 52 s | 44 s | 24 h | 2026-10-11 |
| Jellyfin | v1.2.5 → v1.2.6 | 54 s | 11 s | 24 h | 2026-10-11 |
| Jira | v1.8.5 → v1.8.6 | 6 min 12 s | 2 min 1 s | 24 h | 2026-10-11 |
| Joomla | v1.9.5 → v1.9.6 | 1 min 8 s | 34 s | 24 h | 2026-10-11 |
| Keycloak | v1.8.5 → v1.8.6 | 1 min 29 s | 23 s | 24 h | 2026-10-11 |
| Mailu | v1.8.5 → v1.8.6 | 2 min 4 s | 42 s | 24 h | 2026-10-11 |
| Mattermost | v1.9.3 → v1.9.4 | 1 min 58 s | 1 min 2 s | 24 h | 2026-10-11 |
| SQL Server | v1.7.1 → v1.7.2 | 36 s | 1 s | 24 h | 2026-10-11 |
| Navidrome | v1.1.2 → v1.1.3 | 36 s | 11 s | 24 h | 2026-10-11 |
| Nextcloud + ONLYOFFICE | v2.1.6 → v2.1.7 | 2 min 51 s | 1 min 33 s | 24 h | 2026-10-11 |
| Nextcloud | v2.1.4 → v2.1.5 | 2 min 32 s | 1 min 29 s | 24 h | 2026-10-11 |
| Open WebUI + Ollama | the last drill failed, so no time is quoted | 2026-10-11 | |||
| OTRS (Znuny) | the last drill failed, so no time is quoted | 2026-10-11 | |||
| Outline + Keycloak | v2.2.5 → v2.2.6 | 2 min 36 s | 41 s | — | 2026-10-11 |
| ownCloud | v1.9.9 → v1.9.10 | 1 min 34 s | 43 s | 24 h | 2026-10-11 |
| Portainer | v1.7.2 → v1.7.3 | 22 s | 6 s | 24 h | 2026-10-11 |
| Rocket.Chat | v1.8.3 → v1.8.4 | 1 min 35 s | 24 s | 24 h | 2026-10-11 |
| RomM | v1.1.2 → v1.1.3 | 3 min 45 s | 2 min 12 s | 24 h | 2026-10-11 |
| Seafile | v1.9.3 → v1.9.4 | 1 min 23 s | 29 s | 24 h | 2026-10-11 |
| SiYuan | v1.6.4 → v1.6.5 | 17 s | 8 s | 24 h | 2026-10-11 |
| SonarQube | v1.9.2 → v1.9.3 | 3 min 14 s | 1 min 40 s | 24 h | 2026-10-11 |
| Uptime Kuma | v1.1.2 → v1.1.3 | 53 s | 10 s | 24 h | 2026-10-11 |
| Vaultwarden | v1.7.2 → v1.7.3 | 35 s | 11 s | 24 h | 2026-10-11 |
| Wiki.js | v2.1.3 → v2.1.4 | 51 s | 11 s | 24 h | 2026-10-11 |
| WordPress | v1.10.4 → v1.10.5 | 1 min 19 s | 35 s | 24 h | 2026-10-11 |
| XWiki | v1.9.4 → v1.9.5 | 2 min 14 s | 58 s | 24 h | 2026-10-11 |
| Zabbix (no proxy) | v1.9.3 → v1.9.4 | 59 s | 14 s | 24 h | 2026-10-11 |
| Zabbix | v1.10.2 → v1.10.3 | 54 s | 14 s | 24 h | 2026-10-11 |
A drill that fails is listed as failing and carries no time. A restore time from a restore that did not work is not a time.
In the words of a vendor questionnaire. The recovery time objective is the measured column, not a target somebody wrote down. The recovery point is the backup interval each template ships with, read from the file an operator copies; shortening it is one variable. The date of the last recovery test is the last column, and while the drill stays green it is never more than a week old.
Scored by someone else
OpenSSF Scorecard, across the whole fleet, low marks included
Scorecard runs its own checks against every public repository here and publishes the result. The fleet reads that result each morning and repeats it, rather than awarding itself a number. Median 7.3 of 10 across 100 repositories; the lowest is 6.5, fleet-ops. Below, every check that is under ten somewhere, with how many repositories and what that means here. A check Scorecard marks as not applicable is not counted.
| Check | Below ten | What it means here |
|---|---|---|
Branch-Protection | 100 | Until 24 September no rule stopped main being rewritten. Every default branch now blocks force-push and deletion; required reviews are refused on purpose. Scorecard re-scores weekly, so this count lags the rollout. |
CII-Best-Practices | 100 | Filled in on 25 September for every repository that is not documentation alone, with the answers public; the section below says which are short of passing and why. Scorecard re-reads the badge site weekly, so this count lags. |
Code-Review | 100 | One maintainer and an agent. Nothing merges on a second person's review; the review is CI against planted violations, and this row will stay at zero for as long as that is true. |
Contributors | 100 | No reading yet. Listed, not excused. |
Fuzzing | 100 | There is no code here to fuzz: compose files, shell, and workflows. |
Pinned-Dependencies | 100 | One line: the SLSA generator that signs every release is referenced by tag, because it halts when referenced by commit. Every other action in the fleet is pinned by commit SHA. Scorecard scores that line as 8 of 10 on every repository that carries the release workflow, and the trade was made knowingly; see decision 17. |
Maintained | 37 | Scorecard scores zero for any repository younger than ninety days whatever it does, and this summer's wave created most of these. It resolves itself. |
SAST | 31 | The templates upload Trivy results as SARIF, which counts; the tools and documents around them run no scanner. |
CI-Tests | 9 | Repositories with nothing to run on a pull request, mostly documents. Listed, not excused. |
Binary-Artifacts | 3 | No reading yet. Listed, not excused. |
Signed-Releases | 3 | Since 24 September every release carries a git archive, a keyless Sigstore signature and SLSA provenance, and the last five of each repository were given the same. Scorecard re-scores weekly; a repository still here has releases it has not re-read yet. |
License | 2 | No reading yet. Listed, not excused. |
Token-Permissions | 1 | No reading yet. Listed, not excused. |
Read from Scorecard's own API, results dated 2026-10-13. The decision to leave Code-Review at zero, and why, is decision 16.
Answered by me, published by them
OpenSSF Best Practices, with every answer public and the short ones named
The badge is a questionnaire, not a scan: sixty-seven criteria, each answered Met, Unmet or N/A with a sentence saying why, and OpenSSF publishes the answers beside the badge. A badge is only as honest as its answers, so each one here names the workflow or file that makes it true, and a repository with no test suite says Unmet where it is unmet. Passing on 90 of 90 registered repositories.
| Repository | Answered | Why it is short |
|---|---|---|
| Nothing registered is short of passing today. | ||
Read from the badge site's own API, answers last changed 2026-09-27; 0 could not be read at the last count and are in neither number. The 10 public repositories that hold no code to rate are not registered. Why a criterion that is unmet is answered Unmet rather than Met is decision 18.
Threat model
What an attacker, or an ordinary Tuesday, would try, and what answers
A control that is not tied to a threat is a feature. Each row names the failure first, then the control, then where the control is proven to work, which for a public template is a file a reader can open.
| The failure | What answers it | Proven where |
|---|---|---|
| Upstream publishes a different image under the same tag | Every image is pinned by digest; a daily job re-resolves each pin and goes red when upstream moved | freshness.yml |
| A GitHub Action's tag is moved to hostile code | Actions pinned by commit SHA; write permissions on the one job that writes; Dependabot moves the SHA through a pull request that merges only after the repository's own verification is green | dependabot-automerge.yml, Scorecard Token-Permissions and Pinned-Dependencies |
| A vulnerable image ships and nobody looks | Trivy scans every image on every push and daily, results to the Security tab; the digest bump arrives from Dependabot within the week | deployment-verification.yml |
| A credential lands in git | .env is ignored, the compose file refuses to render without its required variables, and a repository whose history ever carried a value says so in its README with the rotation procedure | SECURITY.md, and the pre-rotation advisories in the ledger |
| The host is gone | A weekly restore of the previous release's backup into the current release on a machine that has never run the stack, timed | the table above, dr-drill.yml |
| The backup is running and is a backup of nothing | CI stores a file, backs up, stores a second, restores, and requires the first back and the second gone; the shipped restore script is what runs | case one, case two |
| A database major reaches end of vendor support while every check stays green | A weekly job reads the vendors' own support calendars and names the pin that runs out, and when | fleet-ops, private; the issues it opens are the record |
| A watcher stops, and nothing watches the watcher | A daily heartbeat reads every schedule's own cron and reports one that has not fired; its own run pings a switch outside GitHub that alarms on silence | fleet-ops, private; two of its false alarms are in the ledger |
| Somebody rewrites main | A ruleset on every default branch blocks force-push and deletion; the heartbeat asks for both rules daily | Scorecard Branch-Protection, above; decision 16 |
| The agent that writes most of this gets the keys | Model credentials come from workload identity federation, never a stored key; the agent never reads a decrypted secret and never changes a production server | how the AI is run, decision 7 |
| The tag you deploy is not the tree that was tested | Every release carries a git archive of the tag, a keyless Sigstore signature over it, verified on the runner before upload, and SLSA build provenance; the README says how to check all three with nothing from the repository trusted | release-assets.yml, decision 17 |
| Somebody finds a hole and has nowhere private to say so | A SECURITY.md in every repository with an address, a PGP key, and a seven-day acknowledgment | SECURITY.md |
How the AI is run
The agent is fast, and it is wrong on a schedule. The operating model assumes both.
An AI agent does most of the typing across this fleet, and nothing it says about its own work counts as evidence. The typing is the part anyone can buy now. What cannot be bought is the set of rules that decide what the agent's work is allowed to become.
- Nothing ships on a claim
- A release is cut only after the release commit itself passes a full deploy, backup and restore in CI. The agent's report that something works is not an input.
- A check that cannot fail is not a check
- Every fleet rule is exercised against a planted violation. A rule that stays quiet over its own violation fails the build.
- Exceptions only shrink
- Anything excused from a rule sits on a written list. An item that has since been fixed but is still listed is itself a failure, so the list cannot quietly become permanent.
- The agent does not hold the keys
- It never reads decrypted secrets and never changes a production server. A server is changed only by the agent that lives on it, from a written brief, and it reports back what it did.
- Its mistakes are published
- The public record of findings includes the agent's own, among them a cause it published before measuring it, and the correction.
Caught before release on 23 September 2026. Five mistakes by the agent in one day of work across 47 repositories. None of them reached a release.
| What the agent got wrong | What caught it | Proof |
|---|---|---|
| An apostrophe in a comment closed a shell quote, so Outline's backup loop never started. | CI waits for a "Data backup OK" line, not for a running container | fix |
| A bulk rename also renamed an unrelated variable in five test suites. | strict mode stopped every one of those runs | fix |
| A new restore script was committed without its executable bit, and the same test exported the environment file, so the stack rendered without one. | the test runs the shipped file, and the check that the stack must refuse to render | fix |
| A generated restore called a database client the image does not ship. | the first CI run of the shipped script | fix |
| Tests passed locally against a stale copy of the reference repository. | CI, which checks against the current one | private repository |
Green is a claim. A restore is evidence.