docs: Gate 5 for #0106 — half the estate had never been asked

Deployed by sorb, then measured rather than assumed: 56 targets, coverage at a
hundred percent, no orphaned report, and the round completing in about two
minutes. Every blind spot named in the issue now carries a report and both stale
entries are gone.

The news is not that the pipeline was broken. It answered reliably for the
twenty-nine images it knew about. The other twenty-seven had never been asked,
and they carry a hundred and twenty-seven additional critical findings —
including the public wiki, both Traefik layers, Prometheus, Grafana, Alertmanager
and the scanner's own image. That belongs to #0051, which is updated with the
new baseline; one of its candidates is not fixable but removable, since an
unused registry tag only entered the set because its repository has fewer tags
than the selection depth.

Three of my own numbers were wrong and are corrected in place: sixty-five
targets became fifty-six, up to sixty-five messages became thirty-nine, and the
normalisation changed every alert fingerprint so all known findings reported
once more. The last one was foreseeable from Gate 2, where normalisation is
already listed as mandatory; I did not follow it through to the alerting layer.

The shakiest call of Gate 3 measured better than feared. cAdvisor sees all eight
images the compose file declares and four more that appear in no compose file of
ours — the registry itself among them. Deriving from the running host beats
deriving from its description.

A new stolperstein carries the cheapest lesson of the day: a test that rebuilds
the unit under test proves the rebuild. Loading the real function exposed within
one run that the shipped file ignored its environment entirely.
This commit is contained in:
Thore Cimbal
2026-08-21 12:00:00 +00:00
parent c4e440dc1d
commit e342229823
6 changed files with 236 additions and 4 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ als Wiki-Seiten unter [docs/wiki/stolpersteine/](docs/wiki/index.md)
— eine Seite je Muster, mit Stand und, wo zutreffend, der Version,
die es abdeckt (ADR-0009 des Rahmenwerks).
**Die geclusterte Übersicht steht in [STATUS.md](STATUS.md)**, Abschnitt *Fehlerklassen*: 6 offen, 3 teilweise, 4 geerntet, 2 nicht abgedeckt.
**Die geclusterte Übersicht steht in [STATUS.md](STATUS.md)**, Abschnitt *Fehlerklassen*: 7 offen, 3 teilweise, 4 geerntet, 2 nicht abgedeckt.
Diese Datei hält keinen eigenen Bestand mehr. Sie bleibt als
Wegweiser bestehen, weil angenommene Entscheidungen auf sie
+3 -2
View File
@@ -128,15 +128,16 @@ Bedeutung der Meilensteine: siehe [roadmap.md](roadmap.md).
- [AAR — Game-Host-Anbindung: Deploy über zwei Übergaben, Ursache war nie die Firewall](docs/aar/2026-08-20-game-host-anbindung.md)
- [AAR — rohana lief voll, der ThreadNet-Web-Spiegel riss](docs/aar/2026-08-20-rohana-platte-voll-spiegel-gerissen.md)
## Fehlerklassen (17)
## Fehlerklassen (18)
### offen (6)
### offen (7)
- [Die AAR-Pflicht setzt sich ohne Werkzeug nicht durch](docs/wiki/stolpersteine/aar-pflicht-ohne-werkzeug.md)
- [Prüfungen erzeugen Befunde, die niemand beheben kann](docs/wiki/stolpersteine/befunde-die-niemand-beheben-kann.md)
- [Ein gleichzeitiger, sachfremder Defekt wird der eigenen Änderung angelastet](docs/wiki/stolpersteine/fremder-defekt-der-aenderung-angelastet.md)
- [Issues behaupten Zustände, die längst überholt sind](docs/wiki/stolpersteine/issues-behaupten-ueberholte-zustaende.md)
- [Eine Datei kann nicht maßgeblich und zugleich unveränderlich sein](docs/wiki/stolpersteine/massgeblich-aber-unveraenderlich.md)
- [Ein nachgebauter Test prüft den Nachbau](docs/wiki/stolpersteine/nachgebauter-test-prueft-den-nachbau.md)
- [Eine Quittung bezeugt Aufmerksamkeit, nicht Vollständigkeit](docs/wiki/stolpersteine/quittung-bezeugt-aufmerksamkeit.md)
### teilweise gedeckt (3)
@@ -599,3 +599,115 @@ Zusicherungen grün · **13** Shell-Zusicherungen grün · `sh -n` beide ·
**Weicht 13 ab, ist die Herleitung falsch — nicht die Handmessung überholt.**
Dann nicht weiterfahren, sondern den Unterschied ausmessen.
### Slice 5 — durchgeführt, 2026-08-21 · Status: **DONE**
**5.1 — die wackeligste Annahme aus Gate 3, gemessen.** Befürchtet war: cAdvisor
sieht nur, was *läuft*; ein gestoppter Dienst fehlt in der Soll-Menge, und die
Deckung meldet trotzdem 100 %.
| | Zahl |
|---|---|
| im Compose deklarierte Images | 8 |
| davon von cAdvisor gesehen | **8** — Lücke **0** |
| zusätzlich gesehen, in **keinem** unserer Compose-Dateien | **4** |
Die vier sind `gitea/gitea:1.27.0` (**die Registry selbst**), `traefik:v3.7.9`,
`portainer/agent:2.27.5` und `cadvisor`. **Eine aus dem Compose abgeleitete
Liste hätte genau diese vier übersehen** — cAdvisor ist also nicht der
schwächere, sondern der stärkere Weg. Das Restrisiko (ein gerade gestoppter
Dienst) bleibt und ist heute mit **0** beziffert statt vermutet.
**5.2 — Kriterium 5** zeigt sich beim nächsten `threadnet-web`-Release von
selbst und ist bis dahin offen.
## Gate 5 — Closeout (AAR)
**Planned vs. actual.** Geplant war ein Vorhaben, das eine Alarmregel gegen eine
handgepflegte Liste baut. Nach sorbs Einwand wurde daraus der Wegfall der Liste
— und damit ein anderes, kleineres und besseres Vorhaben. Fünf Slices statt
einer Regel.
| Abnahmekriterium (Gate 1) | Ergebnis |
|---|---|
| 1 `images.txt` existiert nicht mehr | erfüllt, ersatzlos |
| 2 Deckung 100 % | erfüllt: `cve_targets_missing` **0** bei 56 Zielen |
| 3 kein Ziel ohne Grund | erfüllt: `orphaned` **0**; `v0.3.0` und `coturn:latest` sind fort |
| 4 Deckung als Metrik, alarmierbar | erfüllt: fünf Serien, vier Regeln, drei Panels |
| 5 ein Ausrollen genügt als Nachweis | **offen** bis zum nächsten Release |
| 6 `gameserver_cadvisor` bleibt draußen | erfüllt |
**Der Ertrag, gemessen statt behauptet.**
| | vorher (29) | nachher (56) |
|---|---|---|
| Deckung | 52 % | **100 %** |
| CRITICAL | 126 | **253** |
| HIGH | 1222 | **3674** |
⚠️ **Die zuvor unsichtbare Hälfte trägt 127 zusätzliche CRITICAL-Befunde.** Zum
ersten Mal überhaupt gemeldet: das öffentlich erreichbare Wiki, **beide**
Traefik-Schichten, Prometheus, Grafana, Alertmanager, die k3s-Bausteine — und
`aquasec/trivy` selbst. Nicht die Pipeline war kaputt; die Hälfte des Bestands
war nie befragt worden.
**Was diese Arbeit falsch gemacht hat.**
1. ⚠️ **Mein erster Vorschlag war eine Krücke.** Eine Regel, die Abweichungen
einer gepflegten Liste meldet, hält die Liste am Leben und fügt ihr eine
Pflicht hinzu. sorbs Einwand („das muss automatisch passieren") hat das
Vorhaben halbiert und verbessert. Ich hatte die Leiter nur bis „was fehlt in
der Liste" bestiegen, nicht bis „braucht es die Liste".
2. **Die Mengenschätzung war falsch.** „~65 Ziele" beruhte auf drei Tags je
Repo; vier der sechs Repos haben nur **einen**. Tatsächlich 56 — erklärbar
(51 laufend + 9 Registry 4 Überschneidung), aber eben nachträglich.
Ebenso „≤ 65 Meldungen": es waren **39**.
3. ⚠️ **Ein Nebeneffekt, den ich hätte vorhersehen können.** Die Normalisierung
ändert die `target`-Beschriftung (`docker.io/postgres:17-alpine`
`postgres:17-alpine`) und damit **jeden Alarm-Fingerabdruck**. Alle
bekannten Befunde galten einmalig als neu und wurden erneut gemeldet. Das
steht in keinem der Gates, obwohl Gate 2 die Normalisierung ausdrücklich als
Pflicht führt — ich habe ihre Wirkung auf die Alarmschicht nicht zu Ende
gedacht.
4. **Drei Einrückungs- und Gruppierungsfehler** (Compose 4/6 statt 6/8, Alarme 6
statt 8, `runde || sleep A && sleep B`). Alle von Kontrollen gefangen, keiner
ausgeliefert.
**Why the difference.** Die drei Korrekturen, die etwas geändert haben, kamen
von außen oder aus einer Messung: sorbs Einwand, der fehlgeschlagene Testlauf,
der Vergleich Compose gegen cAdvisor. Keine kam aus dem Nachdenken über den
eigenen Plan.
**Learnings.**
- ⚠️ **Ein nachgebauter Test prüft den Nachbau.** Der erste `test_scan_loop.sh`
baute die Schleifenlogik nach und wäre grün geblieben, während die
ausgelieferte Datei kaputt ist. Nach dem Umbau auf *Laden* der echten
`runde()` fiel binnen **eines Laufs** auf, dass `scan-loop.sh` seine Pfade
fest verdrahtete. Das ist die billigste Lehre des Tages und die
übertragbarste.
- **Eine Prüfung schuldet den Nachweis, dass sie rot werden kann** — hier
viermal geführt (Normalisierung sabotiert: 11 von 21 rot; Alarmschwelle
unerreichbar: rot; Karenz entfernt: rot; Wache entfernt: „Berichte
unberührt" fällt von 2 auf 0).
- **Wer Ziele ableitet, muss das Ausbleiben der Herleitung alarmieren.** Eine
leere Soll-Menge sieht aus wie vollständige Deckung. Das ist der Kern von
ADR-0026 und der Grund für `cve_target_source_stale`.
- **Die laufende Anlage ist die bessere Quelle als ihre Beschreibung.** cAdvisor
sah vier Images, die in **keiner** unserer Compose-Dateien stehen — darunter
die Registry selbst.
**Harvested.** ADR-0026. Für die nächste Ernte vorgemerkt: die Lehre über
nachgebaute Tests, als eigener Stolperstein.
**Open uncertainties.**
1. **Kriterium 5** ist bis zum nächsten Release unbewiesen.
2. ⚠️ **127 neu sichtbare CRITICAL-Befunde sind unbearbeitet.** Sie gehören zu
#0051, nicht hierher — aber sie sind ab heute sichtbar und laufen auf.
3. **Ein gestoppter Dienst fehlt weiterhin still in der Soll-Menge.** Heute mit
0 beziffert, morgen nicht zwingend.
4. **Die Auswahl „letzte drei je Repo" ist grob.** Bei `axion-backup` sind das
beide Tags, auch der ungenutzte `v1` — der prompt CRITICAL meldet.
> **STOP — Freigabe für Gate 5.**
+30 -1
View File
@@ -6,7 +6,7 @@ created: 2026-08-14
milestone: M5
priority: high
area: security
related: [docs/issues/0025-deploy-uebergabe-cve-alarme-aggregiert-receiver.md, docs/aar/2026-08-01-cve-pipeline-gitops47.md]
related: [docs/issues/0025-deploy-uebergabe-cve-alarme-aggregiert-receiver.md, docs/aar/2026-08-01-cve-pipeline-gitops47.md, docs/issues/0106-cve-scan-deckt-nur-die-haelfte-zielliste-ist-handgepflegt.md]
gitlab_iid: "37"
---
# CVE-Remediation-Pass: Schwachstellen-Report abarbeiten
@@ -32,3 +32,32 @@ nur die Alarm-Zustellung ab — die eigentliche **Behebung** fehlte als eigenes
Hängt eng an der Update-Kadenz (#0052) — Updaten ist der Haupthebel gegen die CVEs.
Methode/Runbook: `/betrieb/sicherheit`; Update-Prozess: `/betrieb/upgrades`.
## ⚠️ Die Grundmenge hat sich am 2026-08-21 verdoppelt (#0106)
Bis dahin beruhte jede Zahl in diesem Issue auf **29** gescannten Images. Das
waren 52 % des Bestands — die Zielliste war handgepflegt und lief dem Ausrollen
hinterher. Seit #0106 wird sie abgeleitet, es sind **56** Ziele, und die Deckung
liegt bei 100 %.
| | vorher (29 Images) | jetzt (56 Images) |
|---|---|---|
| CRITICAL | 126 | **253** |
| HIGH | 1222 | **3674** |
**Der Zuwachs ist kein neuer Schaden, sondern vorher unsichtbarer Bestand.**
Erstmals überhaupt erfasst und sofort mit CRITICAL-Befunden dabei: das
öffentlich erreichbare Wiki, **beide** Traefik-Schichten (Cluster und
Betriebs-Host), Prometheus, Grafana, Alertmanager, die k3s-Bausteine, mehrere
Postgres-Fassungen — und `aquasec/trivy` selbst.
Für diesen Remediation-Pass heißt das: Der Umfang ist rund doppelt so groß wie
bei der Erstellung dieses Issues angenommen, und die neu hinzugekommenen
Kandidaten sind die exponierteren (Ingress, öffentliches Wiki), nicht die
harmloseren.
⚠️ **Ein Kandidat ist vermutlich gar nicht zu beheben, sondern zu entfernen:**
`rohana.axion1337.de/sorb/axion-backup:v1` meldet CRITICAL, wird aber nirgends
betrieben — er steht nur in der Zielmenge, weil die Registry-Auswahl „die
letzten drei Fassungen je Repo" nimmt und das Repo nur zwei hat. Ein
Registry-Aufräumen wäre hier die billigere Antwort als ein Update.
@@ -23,6 +23,8 @@ related:
| slice | commit | status | note |
|---|---|---|---|
| 5 | | | OPEN | Wartet auf Freigabe; Ausrollen durch sorb, Deckung 100 % bei 56 Zielen gemessen. |
| 5 | (slice) | DONE | cAdvisor gegen docker compose config: Luecke 0, und vier Images gesehen, die in keinem Compose stehen. |
| 4 | 07875ba | DONE | Berichte ausgefallener Ziele entfernt; Test laedt die echte runde() statt sie nachzubauen. |
| 3 | 386b65a | DONE | Scanner liest die abgeleitete Menge, images.txt geloescht (Kriterium 1). |
| 2 | b989987 | DONE | Vier Regeln statt drei; erste Regeltests im Stack, zwei Sabotagen belegen sie. |
@@ -0,0 +1,88 @@
---
type: wiki-page
area: stolpersteine
status: open
sources:
- "docs/design/2026-08-21-cve-ziele-ableiten.md"
related:
- "docs/wiki/stolpersteine/meldet-erfolg-ist-aber-blind.md"
- "docs/wiki/stolpersteine/fremder-defekt-der-aenderung-angelastet.md"
---
# Ein nachgebauter Test prüft den Nachbau
> **Stand: offen.** Noch nicht an das Rahmenwerk übergeben.
Ein Test, der die zu prüfende Logik **nachbildet**, statt sie zu **laden**,
prüft die Nachbildung. Er kann dauerhaft grün bleiben, während die ausgelieferte
Datei kaputt ist — und er sieht dabei genauso aus wie ein richtiger Test:
gleiche Zusicherungen, gleiche Ausgabe, gleiche Zeile im Abschlussbericht.
Das ist der Sonderfall von [[meldet-erfolg-ist-aber-blind]], der die Prüfung
selbst trifft statt das Geprüfte.
## Das belegte Vorkommen
Am **2026-08-21** entstand ein Shell-Test für eine Scan-Schleife. Die Schleife
lebt in einer Datei, die als Endlosschleife startet und deshalb nicht ohne
Weiteres geladen werden kann. Der bequeme Ausweg: den Rumpf im Test noch einmal
hinschreiben, „wortgleich zur echten Datei".
**Dreizehn Zusicherungen liefen grün.** Darunter der wichtigste Fall überhaupt —
*„ohne Zielliste darf nichts gelöscht werden"*, dessen Verletzung den gesamten
Berichtsbestand vernichtet hätte.
Beim Umbau auf *Laden* der echten Funktion (Ladewache über eine
Umgebungsvariable, die den Treiber am Anlaufen hindert) fielen **binnen eines
einzigen Laufs** fünf Zusicherungen um: Die ausgelieferte Datei setzte ihre
Pfade **fest** und ignorierte die Umgebung vollständig. Der Nachbau tat es
nicht, weil im Nachbau die Pfade aus dem Test kamen.
Der Unterschied war also nicht theoretisch. Er war schon da, und der grüne Test
hat ihn zugedeckt.
## Warum es so verführerisch ist
Der Nachbau ist fast immer der **kürzere** Weg, und er sieht aus wie
Sorgfalt — schließlich steht die Logik zweimal da, einmal geprüft. Drei
Umstände machen ihn attraktiv:
1. **Die Datei lässt sich nicht laden**, weil sie beim Laden etwas tut
(Endlosschleife, Serverstart, Seiteneffekt beim Import).
2. **Der Nachbau ist beim Schreiben korrekt.** Er entsteht ja aus derselben
Vorlage; die Abweichung entsteht erst später, oder — wie hier — sie war von
Anfang an da und fiel nicht auf, weil beide Fassungen nebeneinander gelesen
und für gleich gehalten wurden.
3. **Der Test wird nie wieder angefasst.** Die geprüfte Datei ändert sich, der
Nachbau nicht. Ab da prüft er eine Vergangenheit.
## Was stattdessen zu tun ist
**Die zu prüfende Einheit muss ladbar sein, und der Test muss sie laden.**
Ist die Datei nicht ladbar, ist *das* der zu behebende Mangel — nicht ein
Grund für einen Nachbau. Die übliche Lösung kostet drei Zeilen:
- Den Rumpf in eine **Funktion** heben und den Treiber ans Ende stellen.
- Eine **Ladewache** davor, die den Treiber überspringt, wenn eine
Umgebungsvariable gesetzt ist (`[ "${X_NUR_LADEN:-0}" = "1" ] && return 0`).
In Python ist das `if __name__ == "__main__":` — dieselbe Sache, nur
eingebaut.
- Die Berührpunkte nach außen (Netzaufruf, Unterprozess) als **eigene
Funktionen**, die der Test überschreibt. Nicht die Logik nachbauen, nur die
Ränder ersetzen.
**Die Gegenprobe, die den Unterschied sichtbar macht:** Die geprüfte Datei
absichtlich kaputt machen und schauen, ob der Test rot wird. Ein Test, der eine
Nachbildung prüft, bleibt dabei **grün** — und genau daran erkennt man ihn.
## Warum das nicht plattformspezifisch ist
Die Umstände sind allgemein: eine Datei, die beim Laden etwas tut, und ein Test,
der es bequem haben will. Das trifft Shell-Skripte mit Endlosschleife,
Dienste, die beim Import lauschen, Konfigurationen, die im Test „nachgebildet"
statt eingelesen werden, und jede Prüfung, die eine Regel im Testcode noch
einmal formuliert, statt die ausgelieferte Regel zu laden.
Erntewürdig ist die Regel dahinter: **Wenn ein Test die zu prüfende Einheit
nicht laden kann, ist die Einheit zu ändern — nicht der Test zu erfinden.**