--- type: issue id: "0082" status: done created: 2026-08-01 milestone: M1 priority: medium projekt: gitops gitlab_iid: "49" related: [] --- # CVE-Alarme: eine Matrix-Nachricht pro CVE flutet den Security-Raum -- Zustellung derzeit stumm > Adoptiert aus [gitops#49](https://git.lab/axion1337.chat/axion1337.chat-gitops/-/issues/49) (2026-08-18, ADR-0019). Kommentare und Verlauf bleiben dort; kanonisch ist ab jetzt diese Datei. Aus dem Deploy von #47 (2026-08-01). Die Pipeline sammelt Daten, **die Alarm-Zustellung ist aber abgeklemmt**: in `monitoring/alertmanager/alertmanager.yml` routet `room="security"` auf einen Null-Receiver (Commit `2b715ca`). ## Warum `TrivyCriticalVuln` und `TrivyHighVuln` erzeugen eine Alarm-Instanz **pro CVE pro Image**. Gemessen am ersten Scan-Durchlauf, bei 14 von 29 Images: | | Anzahl | |---|---| | CRITICAL (feuert sofort, kein `for:`) | 59 | | HIGH (`for: 24h`) | 445 | Hochgerechnet auf alle 29 Images grob 120 CRITICAL / 900 HIGH. `group_by: [alertname, instance]` legt alle in *eine* Gruppe -> ein Webhook-POST mit ~120 Alarmen. `matrix-alerts.py` schickt daraus **eine Matrix-Nachricht pro Alarm**, sequenziell. ## Was es zur Schleife macht `save_state()` steht in `do_POST` **hinter** der Sende-Schleife. Sobald ein Send fehlschlaegt -- Synapse rate-limitet `rc_message` per Default nach ~10 Nachrichten mit 429 -- fliegt die Exception, der State wird **nicht** gespeichert, der Receiver antwortet 502. Alertmanager wiederholt daraufhin die komplette Gruppe, und die Fingerprint-Deduplizierung (`if fp in state: continue`), die genau das verhindern soll, ist beim Retry noch leer. Das wiederholt sich, statt einmalig durchzulaufen. ## Zum Scharfschalten noetig 1. **Zustellung buendeln.** Entweder `matrix-alerts.py` auf eine Sammelnachricht pro Webhook-Batch umbauen (die fuenf Pflichtfelder je CVE als eine Zeile -- bleibt vollstaendig), oder die Regeln auf `count by (target, severity)` aggregieren und die CVE-Details im Dashboard lassen. 2. **State inkrementell speichern**, nach jedem erfolgreichen Send, plus 429-Behandlung mit `Retry-After`. Danach die `room="security"`-Route aus `alertmanager.yml` entfernen. ## Kleinere Punkte aus demselben Review - `TrivyScanStale` kann ein Image, das **nie** erfolgreich gescannt wurde, nicht melden: ohne ersten Report gibt es keine Serie, an der `time() - trivy_last_scan_timestamp` haengen koennte. Ein dauerhaft fehlschlagendes Image bleibt still; `TargetDown` deckt nur den toten Exporter ab. - Der Exporter prunt den First-Seen-State bei **jedem** Scrape. Ein transienter Lesefehler (`except: continue`) loescht die Erstfund-Zeitstempel des betroffenen Targets dauerhaft. - `coturn/coturn:latest` ist als einziges Image ungepinnt (schon in #47 notiert). ## Nicht betroffen Scanner, Exporter, Scrape-Job und Dashboard laufen und sind verifiziert -- Exporter-Last 0,4 s pro Scrape fuer 29 Reports, unkritisch bei 15 s Intervall. Details im `monitoring/README.md`. --- *Migriert aus Gitea `sorb/axion1337.chat-gitops#51` — dort erstellt am 2026-08-01 von sorb.* ## Richtigstellung + Abschluss 2026-08-19 **Die Kernaussage des Issues war überholt.** „Die Alarm-Zustellung ist abgeklemmt" stimmte am Tag der Erstellung — und wurde noch **am selben Tag** behoben, ohne dass das Issue geschlossen wurde. Ich habe es zwischenzeitlich als Produktionsblocker geführt („CVE-Alarme werden gar nicht zugestellt"); das war falsch, nachgeprüft am Code: | Forderung | Stand | |---|---| | Zustellung bündeln | ✅ Weg 2 gewählt: Regeln aggregieren per `count by (target, target_type, host)`, Details im Dashboard (`alerts.yml`) | | State inkrementell speichern | ✅ `save_state()` steht **in** der Schleife, auch im Fehlerzweig; dazu `time.sleep(1)` gegen `rc_message` | | `room="security"`-Null-Route entfernen | ✅ In `alertmanager.yml` nicht mehr vorhanden; der Kommentar dort hält die Historie fest | | `coturn:latest` pinnen | ✅ gitops `b4650dc` | Alles vier per Commit `ff87cb2` (2026-08-01) bzw. gitops. ### Heute erledigt: die beiden verbliebenen Review-Punkte **Der Exporter löschte Erstfund-Zeitstempel bei jedem Lesefehler.** `first_seen` wurde bei **jedem** Scrape auf das reduziert, was gerade gesehen wurde — und ein Bericht, der sich nicht parsen ließ, wurde per stillem `continue` übersprungen. Seine Findings kamen damit nicht in `seen_keys`, ihre Zeitstempel waren dauerhaft weg, und „erstmals gesehen" fing danach bei *jetzt* an. Gemeldet hat das nichts. Geprunt wird jetzt nur noch für Targets, deren Bericht in diesem Durchgang **wirklich gelesen** wurde. Beidseitig gegen eine Wegwerf-Ablage belegt, nicht argumentiert: ein unlesbarer Bericht lässt seinen Eintrag stehen (`read_errors 1`), ein lesbarer Bericht ohne das Finding räumt ihn weiterhin ab. **`TrivyScanStale` hat eine Blindstelle, die es prinzipiell nicht schließen kann:** Ein Target ohne je erfolgreichen Bericht hat keine Serie, an der `time() - …` hängen könnte — es bleibt still, egal wie lange es kaputt ist. Dafür verlassen den Exporter jetzt zwei Zahlen (`trivy_reports_total`, `trivy_report_read_errors`) mit je einer Regel: `TrivyReportUnreadable` (>0 für 30 min) und `TrivyNoReports` (==0 für 1 h). Das schließt die Lücke so weit, wie sie **ohne Soll-Liste der erwarteten Targets** zu schließen ist — eine solche Liste wäre der nächste Schritt, ist aber ein eigener Umfang. Commit `9a10615` in `threadnet-operating`.