"Alert delivery is disconnected" was true when the issue was filed and untrue by that evening. I carried it as a production blocker on the strength of the issue text alone, which was wrong: the rules aggregate per target, save_state runs inside the loop including the failure branch, the security null route is gone, and coturn is pinned. All of it verified in the code, not inferred. What genuinely remained were the two smaller review points, and both are done now. The exporter no longer erases first-seen timestamps when a report fails to parse - pruning is limited to targets actually read this round, proven in both directions. And because TrivyScanStale cannot by construction report a target that never produced a report, the exporter now emits trivy_reports_total and trivy_report_read_errors with an alert on each. Priority corrected from high to medium to match what was actually open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
5.4 KiB
Markdown
95 lines
5.4 KiB
Markdown
---
|
|
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.*
|
|
<!-- gitea-migration: sorb/axion1337.chat-gitops#51 -->
|
|
|
|
## 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`.
|