Verkettetes, verifizierbares Audit-Log (GoBD)
Die offene Login-Lücke (siehe „Auth & Security-Härtung") hatte eine unbequeme Nebenwirkung: Wenn Module bis dahin ungeschützt erreichbar waren, konnte ich für keinen einzigen Datensatz belastbar sagen, wer ihn wann geändert hat. Für ein Programm, das die Einnahmen-Überschuss-Rechnung fürs Finanzamt vorbereitet, ist das keine Kleinigkeit – die GoBD verlangen Nachvollziehbarkeit und Unveränderbarkeit. Also habe ich im selben Zug ein Änderungsprotokoll eingebaut.
Erfasst wird zentral über einen EF-Core-SaveChanges-Interceptor: eine Whitelist relevanter Felder, dazu wer (aus dem JWT), wann und was – nicht verstreut über einzelne Service-Methoden, sondern an einer Stelle, die sich nicht umgehen lässt. Das schützt aber nur den Weg über die Anwendung. Wer direkten Datenbankzugriff hat, kann Protokollzeilen genauso ändern wie alles andere – und bei einer self-hosted Software ist der Betreiber selbst genau die Person, deren spätere Manipulation die GoBD-Anforderung der Unveränderbarkeit verhindern soll.
Deshalb liegt über dem Protokoll eine SHA-256-Hashkette: jede Zeile hasht ihren eigenen Inhalt zusammen mit dem Hash der Vorgängerzeile. Die erste Zeile knüpft an einen festen Genesis-Wert an. Ein direkter Eingriff in eine Zeile bricht ihren Hash – und über die Verkettung auch die Hashes aller nachfolgenden Zeilen. Ein Append-only-Trigger auf DB-Ebene und ein SELECT … FOR UPDATE auf die jeweils letzte Zeile (damit parallele Schreiber die Kette nicht gabeln) sichern das ab.
Entscheidend war die Erkenntnis, dass eine gespeicherte Kette wertlos ist, solange sie niemand nachrechnet: Eine Verifikationsroutine läuft von Genesis bis zur letzten Zeile durch und meldet den ersten Bruch – wahlweise, weil der Inhalt einer Zeile nicht mehr zu ihrem Hash passt, oder weil die Verkettung zur Nachbarzeile nicht mehr stimmt (Zeile entfernt, eingefügt, umsortiert). Für Bestandssysteme, die vor Einführung der Kette schon Protokollzeilen hatten, war zusätzlich Arbeit nötig, damit ein Upgrade nicht fälschlich einen Manipulationsalarm auslöst.
Ehrlich zur Grenze: Eine schlüssellose Hashkette, die vollständig in derselben Datenbank liegt, erhöht den Aufwand einer Fälschung – sie macht sie nicht unmöglich. Wer Vollzugriff auf die DB hat und den (offenen) Quellcode kennt, kann die Kette ab der geänderten Stelle neu berechnen. Für ein self-hosted System, in dem ein Angreifer dafür erst Server und Container-Stack übernehmen müsste, halte ich dieses Restrisiko für vertretbar – und schreibe in der Verfahrensdokumentation bewusst „unbefugte und versehentliche Änderungen werden nachweisbar", nicht „manipulationssicher". Das eine Wort zu viel wäre gefährlicher als die fehlende Funktion.