feat: tools/waechter.ps1 -- zaehlt offene Tueren und rechnet Dateien nach

Die Statusseite war waehrend des Einbruchs die ganze Zeit gruen. Sie waere auch
gruen geblieben, wenn der Miner gelaufen waere. Es fehlte kein Bildschirm, auf
dem man es haette sehen koennen -- es fehlte etwas, das VON SICH AUS Bescheid
sagt. Deshalb kein Frontend, sondern zwei Pruefungen mit Meldeweg.

  1. Ports, die auf allen Adressen lauschen, gegen einen hinterlegten Stand.
     Port 3000 stand elf Tage offen; das haette am ersten Tag gemeldet werden
     koennen. Dazu eine feste Liste heikler Ports, die auch dann warnt, wenn
     der Zustand "schon immer so" war -- 3000 war auch schon immer so.
  2. Pruefsummen ueber sshd_config, administrators_authorized_keys, beide
     Compose-Dateien und Giteas app.ini und .gitconfig. Der Angriff WAR eine
     Datei, die sich um 21:13:57 aenderte.

Gemeldet wird nur bei Aenderung, mit Merker in den Einstellungen. Ein Waechter,
der taeglich meckert, wird stummgeschaltet und ist dann schlechter als keiner.

VOR DEM EINRICHTEN AN BEIDEN FAELLEN BEWIESEN statt behauptet:

    Testport 45999 geoeffnet  ->  "NEU offen: 45999"
    Datei geaendert           ->  Meldung raus, HTTP 204
    zurueckgestellt           ->  "Wieder in Ordnung (Merker geleert)"

Und er prueft zuerst, ob er ueberhaupt etwas sehen kann: findet er keine
Objekte, meldet er "ungueltig" statt "sauber". Das ist die Lehre aus zwei
Suchen in dieser Nacht, die genau das verwechselt haben.

Die Statusseite zeigt den Stand mit; meldet der Waechter sich zwei Stunden
nicht, ist DAS der Befund -- er laeuft alle 15 Minuten.

Im Bericht ausserdem nachgetragen: die Inventur von aussen (NPM-Verwaltung und
MariaDB standen offen, beide jetzt gesperrt), die abgeschalteten Ueberbleibsel
von Caddy und cloudflared, und zwei Irrtuemer von mir -- "Docker geht an der
Firewall vorbei" und "der MariaDB-root hat kein Passwort". Beide waren aus
Messungen geschlossen, die die Frage nicht beantworten konnten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 09:22:44 +02:00
co-authored by Claude Opus 5
parent 70260060c9
commit f138df44a7
4 changed files with 370 additions and 0 deletions
+53
View File
@@ -221,6 +221,59 @@ async function pruefen(): Promise<Antwort> {
}
}
// -- Der Waechter ---------------------------------------------------------
//
// Geschrieben von tools/waechter.ps1 auf dem WIRT -- der Dienst im Container
// sieht weder die Ports der Maschine noch ihre Dateien. Hier steht nur, was
// der Waechter zuletzt gefunden hat.
//
// FEHLT DER EINTRAG GANZ, ist der Waechter nicht eingerichtet. Das ist kein
// Fehler des Dienstes, aber es gehoert gesagt: eine Statusseite, die
// schweigt, weil niemand hinsieht, ist genau das Problem, gegen das der
// Waechter gebaut wurde.
const wRoh = getSetting('waechter.letzte')
if (!wRoh) {
pruefungen.push({ was: 'waechter', ok: true, hinweis: 'nicht eingerichtet' })
} else {
try {
const w = JSON.parse(wRoh) as {
at: number
ok: boolean
ports?: number
heikel?: number[]
befund?: string[]
}
const stunden = (now() - w.at) / 3_600_000
if (stunden > 2) {
// Er laeuft alle 15 Minuten. Zwei Stunden Schweigen heisst: er
// laeuft nicht mehr -- und ein Waechter, von dem man glaubt,
// dass er wacht, ist schlimmer als keiner.
pruefungen.push({
was: 'waechter',
ok: false,
hinweis: `meldet sich seit ${Math.round(stunden)} Stunden nicht`,
})
} else if (!w.ok) {
pruefungen.push({
was: 'waechter',
ok: false,
hinweis: (w.befund ?? []).join('; ') || 'Befund ohne Text',
})
} else {
const heikel = w.heikel?.length
pruefungen.push({
was: 'waechter',
ok: true,
hinweis: heikel
? `${w.ports} Ports offen, davon ${heikel} heikel`
: `${w.ports} Ports offen`,
})
}
} catch {
pruefungen.push({ was: 'waechter', ok: false, hinweis: 'Eintrag unlesbar' })
}
}
// -- Das Urteil -----------------------------------------------------------
//
// ZWEI STUFEN, und der Unterschied ist der zwischen "tut es nicht" und