docs: Gate 3 for #0106 — eight files, and the five calls I trust least
Gitea's package API wants a token, so the tag timestamps come from the registry itself: manifest, then config blob, then the created field, anonymously, about ninety requests a round. The constraint of no new credentials survives. The derivation is cached rather than run per scrape; at a fifteen second scrape interval it would otherwise make some twenty-one thousand registry requests a day. Two assertions carry the design. A source that fails must leave its own share empty while the others keep delivering, and when every source fails the target file is not overwritten at all. Deleting reports follows the same rule: no target file, no deletion, or a restart during a Prometheus outage would clear the whole estate. The shakiest call is written down as such: cAdvisor only sees containers that run, so a service that happens to be down is missing from the desired set and coverage still reads a hundred percent. That is the hole ADR-0026 warns about, and Gate 4 has to check it against docker compose config before criterion 2 counts as met.
This commit is contained in:
@@ -86,7 +86,7 @@ Bedeutung der Meilensteine: siehe [roadmap.md](roadmap.md).
|
||||
|
||||
| Design | Gate | Title |
|
||||
|---|---|---|
|
||||
| [2026-08-21-cve-ziele-ableiten](docs/design/2026-08-21-cve-ziele-ableiten.md) | gate-2 | Design: Die CVE-Zielmenge ableiten statt pflegen (#0106) |
|
||||
| [2026-08-21-cve-ziele-ableiten](docs/design/2026-08-21-cve-ziele-ableiten.md) | gate-3 | Design: Die CVE-Zielmenge ableiten statt pflegen (#0106) |
|
||||
|
||||
## ADRs (26)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
type: design
|
||||
status: gate-2
|
||||
status: gate-3
|
||||
date: 2026-08-21
|
||||
size: L
|
||||
related:
|
||||
@@ -233,4 +233,138 @@ hinaus — es ist die allgemeine Form dessen, was hier schiefging. Als
|
||||
Ziele ableitet, muss das **Ausbleiben der Herleitung** alarmieren, weil eine
|
||||
leere Soll-Menge sonst wie vollständige Deckung aussieht.
|
||||
|
||||
> **STOP — Freigabe für Gate 2.**
|
||||
> **Gate 2 freigegeben durch sorb, 2026-08-21.**
|
||||
|
||||
## Gate 3 — Program Design
|
||||
|
||||
### Dateien
|
||||
|
||||
Alle im Repo `threadnet-operating`, alle unter `monitoring/`.
|
||||
|
||||
| Pfad | Art | Was |
|
||||
|---|---|---|
|
||||
| `cve/targets.py` | **neu** | Herleitung der Soll-Menge; stdlib, importierbar |
|
||||
| `cve/cve-exporter.py` | geändert | ruft die Herleitung, schreibt `targets.txt`, gibt die fünf neuen Metriken aus |
|
||||
| `cve/scan-loop.sh` | geändert | liest `targets.txt` statt `images.txt`; löscht Berichte ausgefallener Ziele |
|
||||
| `cve/images.txt` | **gelöscht** | Kriterium 1 |
|
||||
| `docker-compose.yml` | geändert | Volume `cve_targets` (Exporter rw, Scanner ro); Umgebung für Quellen |
|
||||
| `prometheus/alerts.yml` | geändert | drei Regeln in Gruppe `axion-cve` |
|
||||
| `grafana/dashboards/security/cve-overview.json` | geändert | zwei Panels: Deckung, Frische je Quelle |
|
||||
| `cve/test_targets.py` | **neu** | Zusicherungen, gegen aufgezeichnete Antworten |
|
||||
|
||||
### Signaturen — `cve/targets.py`
|
||||
|
||||
```python
|
||||
NormalisiertesImage = str # ohne docker.io/ und library/, mit Tag
|
||||
|
||||
def normalisiere(ref: str) -> NormalisiertesImage: ...
|
||||
|
||||
def promql(basis_url: str, ausdruck: str, timeout: float = 10.0) -> list[dict]: ...
|
||||
|
||||
def ziele_cluster(basis_url: str) -> set[NormalisiertesImage]: ...
|
||||
def ziele_operating(basis_url: str) -> set[NormalisiertesImage]: ... # job=operating_cadvisor
|
||||
|
||||
def registry_token(registry: str, scope: str) -> str: ...
|
||||
def registry_repos(registry: str) -> list[str]: ...
|
||||
def registry_tags(registry: str, repo: str) -> list[str]: ...
|
||||
def tag_erstellt(registry: str, repo: str, tag: str) -> datetime | None: ...
|
||||
def ziele_registry(registry: str, je_repo: int = 3) -> set[NormalisiertesImage]: ...
|
||||
|
||||
class Herleitung(NamedTuple):
|
||||
ziele: set[NormalisiertesImage]
|
||||
je_quelle: dict[str, set[NormalisiertesImage]] # cluster | operating | registry
|
||||
fehler: dict[str, str] # Quelle -> Meldung
|
||||
stand: dict[str, float] # Quelle -> unix-ts des Erfolgs
|
||||
|
||||
def herleiten(cfg: dict) -> Herleitung: ...
|
||||
```
|
||||
|
||||
### Aufrufweg des Hauptflusses
|
||||
|
||||
```
|
||||
cve-exporter: collect() alle 15 s (Scrape)
|
||||
└─ ziel_cache.hole() gibt zwischengespeicherte Herleitung
|
||||
└─ targets.herleiten(cfg) hoechstens alle SOLL_INTERVALL (Vorgabe 1 h)
|
||||
├─ ziele_cluster() → promql("count by (image) (kube_pod_container_info)")
|
||||
├─ ziele_operating() → promql('...{job="operating_cadvisor",image!=""}')
|
||||
└─ ziele_registry() → registry_repos → registry_tags → tag_erstellt* → juengste 3
|
||||
├─ schreibe targets.txt nur bei Erfolg mindestens einer Quelle
|
||||
└─ metriken: cve_target_desired, _coverage_ratio, _missing, _orphaned, _source_stale
|
||||
|
||||
cve-scan: scan-loop.sh alle 24 h
|
||||
├─ liest /targets/targets.txt fehlt sie → Runde aussetzen, NICHTS loeschen
|
||||
├─ je Ziel: trivy image → /results/<safe>.json
|
||||
└─ loescht /results/*.json ohne Ziel in targets.txt
|
||||
```
|
||||
|
||||
⚠️ **Die Herleitung läuft nicht je Scrape.** Bei 15 s Scrape-Intervall wären das
|
||||
rund 21 000 Registry-Anfragen am Tag. Zwischenspeicher mit eigenem Intervall,
|
||||
die Metriken werden aus dem Zwischenspeicher bedient.
|
||||
|
||||
### Was die Tests zusichern
|
||||
|
||||
**Normalisierung** — `docker.io/library/postgres:17-alpine`, `library/postgres:17-alpine`
|
||||
und `postgres:17-alpine` ergeben denselben Wert; `rohana.axion1337.de/sorb/x:v1`
|
||||
und `ghcr.io/a/b:v1` bleiben unangetastet; ein Ref ohne Tag wird verworfen, nicht
|
||||
zu `:latest` ergänzt (sonst entstünde ein Ziel, das es nicht gibt).
|
||||
|
||||
**Quellentrennung** — aus einer Antwort mit `operating_cadvisor` **und**
|
||||
`gameserver_cadvisor` erscheint kein Ziel der zweiten Menge (Kriterium 6).
|
||||
|
||||
**Registry-Auswahl** — aus Tags mit Zeitstempeln werden die jüngsten drei
|
||||
gewählt, **nach Zeit, nicht nach Namen**: `v0.10.0` neuer als `v0.9.0` muss
|
||||
gewinnen. `sha-*` und `latest*` fallen vorher raus.
|
||||
|
||||
**Ausfallverhalten (der Kern)** — fällt eine Quelle aus, ist ihr Anteil leer,
|
||||
`fehler` trägt die Meldung, `stand` bleibt auf dem alten Wert stehen, **und die
|
||||
übrigen Quellen liefern weiter**. Fallen *alle* aus, ist `ziele` leer und
|
||||
`targets.txt` wird **nicht** überschrieben.
|
||||
|
||||
**Deckungsrechnung** — bei Soll = Ist ist `_missing` 0 und `ratio` 1,0; bei einem
|
||||
fehlenden Ziel ist `_missing` 1; bei einem überzähligen Bericht ist `_orphaned` 1.
|
||||
Gegenprobe mit einem absichtlich falschen Paar, damit die Rechnung nachweislich
|
||||
anschlagen kann (AAR-Lehre: eine Prüfung schuldet den Nachweis, dass sie rot
|
||||
werden kann).
|
||||
|
||||
**Berichts-Löschung** — ein Bericht, dessen Ziel nicht mehr in `targets.txt`
|
||||
steht, wird gelöscht; bei **fehlender** `targets.txt` wird nichts gelöscht.
|
||||
|
||||
### Grenzen — DO NOT CHANGE
|
||||
|
||||
- `alertmanager/matrix-alerts.py`, `alertmanager/alertmanager.yml` — der
|
||||
Meldeweg ist Nicht-Ziel (ADR-0003).
|
||||
- Die bestehenden fünf Regeln `TrivyCriticalVulns`, `TrivyHighVulns`,
|
||||
`TrivyScanStale`, `TrivyReportUnreadable`, `TrivyNoReports` — es kommen
|
||||
Regeln **dazu**, keine wird umformuliert.
|
||||
- Das Metrik-Schema aus #0078 (`trivy_vuln_info`, `trivy_vuln_count`,
|
||||
`trivy_vuln_first_seen_timestamp`, `trivy_last_scan_timestamp`) — nur
|
||||
Ergänzungen.
|
||||
- Die `first_seen`-Zustandsdatei und ihre Prune-Logik.
|
||||
- Alles im Cluster-Repo `gitops`. Dieses Vorhaben fasst nur
|
||||
`threadnet-operating` an.
|
||||
- `game-operating`, `gameserver`, Homelab.
|
||||
|
||||
### Wackeligste Annahmen
|
||||
|
||||
1. ⚠️ **Dass `container_last_seen` den Betriebs-Stack vollständig zeigt.**
|
||||
Gemessen wurden 12 Images — aber cAdvisor sieht nur, was **läuft**. Ein
|
||||
Dienst, der gerade aus ist, fehlt in der Soll-Menge, und die Deckung sähe
|
||||
trotzdem nach 100 % aus. Genau die Lücke, vor der ADR-0026 warnt. Gate 4
|
||||
muss das gegen `docker compose config` gegenprüfen, bevor Kriterium 2 als
|
||||
erfüllt gilt.
|
||||
2. ⚠️ **Dass „die jüngsten drei je Repo" das Richtige treffen.** Bei
|
||||
`threadnet-web` (30 Tags) sind das `v0.6.0`, `v0.5.4`, `v0.5.3` — plausibel.
|
||||
Bei `axion-backup` mit 2 Tags sind es beide, auch wenn `v1` niemand mehr
|
||||
betreibt. Die Regel ist grob; sie ist nur besser als eine Liste.
|
||||
3. **Dass zwei Anfragen je Tag genügen.** Bei einem Multi-Arch-Index sind es
|
||||
drei (Index → Manifest → Config). Gemessen an `v0.6.0`, `v0.5.4`, `v0.1.0`;
|
||||
nicht an allen 36.
|
||||
4. **Dass der Exporter der richtige Ort bleibt.** Er bekommt eine zweite
|
||||
Aufgabe und damit einen zweiten Grund auszufallen. Fällt er aus, fehlen
|
||||
Metriken **und** die Zielmenge veraltet — beides sichtbar, aber gekoppelt.
|
||||
5. **Dass die Löschung von Berichten nicht zu scharf ist.** Ein Ziel, das für
|
||||
eine Runde aus der Soll-Menge fällt (etwa weil ein Pod gerade neu startet),
|
||||
verliert seinen Bericht und damit seine `first_seen`-Historie. Ob das eine
|
||||
Karenz braucht, entscheidet Gate 4 an der Messung.
|
||||
|
||||
> **STOP — Freigabe für Gate 3.**
|
||||
|
||||
@@ -14,7 +14,8 @@ related:
|
||||
|
||||
| gate | commit | approval | status | note |
|
||||
|---|---|---|---|---|
|
||||
| 2 | | | OPEN | Herleitung im Exporter statt drittem Dienst; ADR-0026 vorgeschlagen. |
|
||||
| 3 | | | OPEN | Dateien, Signaturen, Zusicherungen, Grenzen; fuenf wackelige Annahmen benannt. |
|
||||
| 2 | 2693a2d | sorb | DONE | Herleitung im Exporter statt drittem Dienst; ADR-0026 vorgeschlagen. |
|
||||
| 1 | e10cd5d | sorb | DONE | Sechs Kriterien; Registry-Reichweite = Option C (Bestand + letzte drei je Repo). |
|
||||
|
||||
## Ladder
|
||||
@@ -26,6 +27,7 @@ related:
|
||||
| eine Quelle fuer die laufenden Images, statt eine zu bauen | `kube_pod_container_info` (39) und `container_last_seen{job="operating_cadvisor"}` (12) liegen in **derselben** Prometheus, die der Scanner ueber das vorhandene Compose-Netz erreicht | reused: kein neuer Dienst, keine Zugangsdaten, kein neuer Netzweg | |
|
||||
| einen Weg, game-operating auszuschliessen | `job="gameserver_cadvisor"` (20 Images) trennt sauber von `operating_cadvisor` | reused: ein Beschriftungsvergleich statt einer Ausschlussliste | |
|
||||
| ob die Registry ohne neue Zugangsdaten lesbar ist | anonymer Token-Tanz genuegt: `/v2/token` → Katalog und Tag-Listen; 6 Repos, 36 Tags | reused: die Standard-Registry-Authentifizierung, die der Scanner ohnehin schon fuer das Ziehen macht | |
|
||||
| einen anonymen Weg an die Tag-Zeitstempel, bevor ein Token eingeplant wird | Giteas `/api/v1/packages` verlangt einen Token — die Registry-API dagegen nicht: Manifest → Config-Blob liefert `created`, anonym, ~90 Anfragen je Runde | reused: die Registry-Authentifizierung, die der Scanner ohnehin macht — Randbedingung "keine neuen Zugangsdaten" bleibt gewahrt | |
|
||||
| einen Ort fuer die Soll-Menge, bevor ein Dienst dafuer gebaut wird | der `cve-exporter` braucht sie fuer die Deckungsmetrik ohnehin und ist ein laufender stdlib-Python-Prozess im selben Netz | reused: bestehender Prozess statt drittem Container — kein `jq`, kein `pip`, keine zweite Wahrheit | |
|
||||
| eine AAR zu genau diesem Stack, bevor Randbedingungen geraten werden | AAR 2026-08-01: `up -d` aktiviert Configs nicht (im Container pruefen), und `TrivyScanStale` kann ein nie gescanntes Image nicht melden | reused: beide Funde als Randbedingung uebernommen statt neu zu entdecken — Befund 3 bestimmt, dass die Deckung aus der Soll-Menge kommen muss | |
|
||||
| ob die rohana-Berichte ueberhaupt frisch sind | alle 29 Berichte gleichmaessig 4,1 h alt — der Scanner zieht aus der Registry erfolgreich | reused: die bestehende Scan-Schleife bleibt, nur ihre Zielbeschaffung aendert sich | |
|
||||
|
||||
Reference in New Issue
Block a user