Das Änderungsprotokoll (audit_log) ist die technische Vollaufzeichnung der Plattform: jede Änderung an einem
fachlichen Objekt – Sendung, Vorgang, Claim, Dokument, Vertrag, Stammdatum, Benutzer – hinterlässt genau einen
Eintrag. Diese Seite beschreibt, was darin steht, wie die Einträge miteinander verkettet sind und was mit ihnen
geschieht, wenn sie älter werden.
Was in einem Eintrag steht
| Feld | Inhalt |
|---|---|
entity_type, entity_id |
Art und Kennung des geänderten Objekts |
action |
angelegt, geändert, gelöscht |
tenant_id |
der Mandant, dem das Objekt gehört – nicht der Mandant des Aufrufers |
user_id, user_role |
wer die Änderung ausgelöst hat; Hintergrundarbeit (Import, Tracking-Eingang, Anonymisierung) trägt system |
correlation_id |
Kennung der auslösenden Aktion – alle Einträge einer Aktion lassen sich damit zusammen abrufen |
timestamp |
Zeitpunkt in UTC |
changes |
je geändertem Feld der alte und der neue Wert, als JSON |
entry_hash, prev_hash |
die Kette (siehe unten) |
Die Spalte ip_address existiert, wird aber nicht ausgewertet: Hinter dem Ingress trägt sie für jeden Aufrufer
dieselbe interne Adresse. Eine Weiterleitung der Ursprungsadresse (X-Forwarded-For) haben wir bewusst nicht
eingeschaltet, weil sie ohne verlässliche Absenderliste jedem Aufrufer erlauben würde, eine beliebige Adresse zu
behaupten – und ein falscher Wert, den jemand für echt hält, ist schlechter als keiner. Für die Frage „wer hat das
getan?” ist user_id die belastbare Spalte.
Wie ein Eintrag entsteht
Das Protokoll wird nicht von der Oberfläche und nicht von einzelnen Funktionen geschrieben, sondern von einem
Abfangpunkt in der Datenzugriffsschicht (ein SaveChanges-Interceptor des ORM): Beim Speichern werden alle
geänderten Objekte samt alten und neuen Werten eingesammelt und in derselben Datenbanktransaktion wie die
Änderung selbst als Protokollzeilen geschrieben. Schlägt das Protokoll fehl, wird die Änderung nicht gespeichert;
es gibt keine Änderung ohne Eintrag und keinen Eintrag ohne Änderung.
Das gilt auch für Arbeit außerhalb einer Benutzeranfrage – etwa den CSV-Import, den Eingang von Tracking-Ereignissen
oder die Anonymisierung nach Art. 17 DSGVO. Diese Einträge tragen system als Nutzer und den Mandanten des
betroffenen Objekts.
Was bewusst nicht protokolliert wird
- Zugangsgeheimnisse, auch nicht als Hash: Schlüssel-Hashes, Einladungscodes, Carrier-Zugangsdaten und die Konfiguration von Carrier-Konten werden aus den alten und neuen Werten ausgelassen, nicht maskiert. Ein Wert, der genau einmal an genau einer Stelle existieren soll, verliert seinen Sinn, sobald jede Änderung eine Kopie an einem dritten Ort anlegt.
- Rauschen: technische Zeitstempel (
updated_at,last_used_at) und Suchindizes ändern sich bei fast jedem Speichern und beweisen nichts – sie würden die eigentliche Änderung zudecken. - Tracking-Ereignisse und Verbrauchszähler haben eigene, nur anfügende Datenströme. Das Änderungsprotokoll beweist Änderungen; es zählt nicht.
Die Hash-Kette
Jeder Eintrag trägt einen Hash über seinen Inhalt und den Hash des vorherigen Eintrags desselben Mandanten:
entry_hash = SHA-256( prev_hash ⏎ tenant_id ⏎ entity_type ⏎ entity_id ⏎ action ⏎ timestamp ⏎ changes )
prev_hash ist der entry_hash des jüngsten Eintrags dieses Mandanten; der erste Eintrag eines Mandanten hat
keinen Vorgänger. Der Hash wird im selben Schritt wie der Eintrag gebildet – beim Schreiben, nicht nachträglich.
Die Felder sind durch Zeilenumbrüche getrennt. Der Zeitstempel geht in UTC auf die Mikrosekunde ein und changes
in genau der Form, in der PostgreSQL die gespeicherte JSON-Spalte zurückgibt – so rechnet die Prüfung mit denselben
Bytes, die beim Schreiben gehasht wurden. Adress- und Kontaktfelder gehen nur als schlüsselabhängiger Hash ein
(siehe Das Archiv). Bewusst nicht Teil der Eingabe ist die Benutzerkennung: Die Löschung nach
Art. 17 DSGVO ersetzt sie durch ein Pseudonym, und eine rechtmäßige Löschung darf die Kette nicht brechen.
Wird ein Eintrag verändert, stimmt sein Hash nicht mehr; wird einer entfernt, zeigt der nächste auf einen Hash, den
es nicht mehr gibt. Beides ist an genau der Stelle erkennbar, an der es passiert ist.
Die Kette arbeitet ohne geheimen Schlüssel. Sie soll nicht beweisen, wer geschrieben hat – das tun die Zugriffsrechte der Datenbank –, sondern dass die Folge der Einträge vollständig und unverändert ist. Dafür braucht es kein Geheimnis, und ohne Geheimnis kann jeder, der den Export in Händen hält, die Kette prüfen.
Die automatische Prüfung läuft: Eine Integritätsprüfung rechnet die Kette je Mandant nach, benennt jedes gebrochene Glied und läuft jede Nacht (02:30 UTC); das Ergebnis wird gespeichert, ein Bruch löst einen Alarm aus. Sie endet nicht an der Archivgrenze – vor dem Archivieren wird die Kette geprüft, bei Bedarf das Archiv mitgelesen und die Wochen-Prüfsumme nachgerechnet. Auslösen können die Prüfung auch Ihre Administratoren selbst (Recht auf das eigene Protokoll), nicht nur wir.
Einträge, die vor dem 22.09.2026 geschrieben wurden, stammen aus einem Vorläuferverfahren und sind nicht nachrechenbar. Die Prüfung weist sie als solche aus, statt sie zu überspringen. Für Kunden ist das ohne Belang – die Produktivumgebung startet ohne Altbestand –, aber „seit dem ersten Eintrag nachrechenbar“ wäre falsch, und deshalb steht es hier.
Schutz in der Datenbank
Das Protokoll ist wie alle Kundendaten durch Row-Level Security getrennt (siehe Mandantentrennung): Ein Mandant liest nur seine eigenen Einträge; schreiben darf eine Anfrage nur Einträge für den eigenen Mandanten oder – bei Änderungen an globalen Stammdaten, etwa dem Rechtekatalog – mandantenlose Einträge, die niemand außer dem System liest.
Auf der Tabelle liegt außerdem eine Regel, die jedes DELETE ins Leere laufen lässt: Die Anweisung wird
angenommen, löscht aber nichts. Nur der Archivlauf darf löschen – er setzt innerhalb seiner Transaktion ein Signal,
das die Regel für genau diese Transaktion aufhebt, und auch nur für Zeilen, die er zuvor archiviert und zurückgelesen
hat.
Das Archiv
Nach 90 Tagen verlassen Einträge die Datenbank, aber nicht die Plattform. Ein nächtlicher Lauf (02:00 UTC) verschiebt sie in einen eigenen Archivspeicher:
- ein gzip-komprimiertes JSON-Paket je Mandant und Tag:
audit-archive/{mandant}/{jahr}/{monat}/audit_{mandant}_{datum}.json.gz - in drei Stufen: hochladen → zurücklesen und zählen → erst dann löschen. Scheitert eine Stufe, bleibt die Zeile in der Datenbank; ein Archiv wird nie mit einer unvollständigen Fassung überschrieben.
- sonntags zusätzlich eine Merkle-Wurzel über die Woche unter
audit-hash-checkpoints/{jahr}-W{woche}.txt– ein einzelner Hash, mit dem sich später belegen lässt, dass die Wochenpakete unverändert sind.
Der Archivspeicher ist ein eigenes Speicherkonto, getrennt vom Dokumentenspeicher: nur über Identitäten erreichbar (kein Kontoschlüssel), in der Produktivumgebung geografisch redundant gespeichert und mit einer zeitbasierten Unveränderlichkeits-Richtlinie versehen. Was diese Richtlinie leistet und warum sie noch nicht gesperrt ist, steht unter WORM und Aufbewahrung.
Adressen nur als Prüfsumme im Archiv. Ein unveränderliches Archiv und die Anonymisierung von Empfängerdaten nach 180 Tagen (Löschkonzept, Anlage 4 des Vertrags zur Auftragsverarbeitung) passen nur zusammen, wenn das Archiv keine Klartextadressen trägt – aus ihm lässt sich nichts mehr löschen. Deshalb gehen Name, Firma, Straße, Ort, E-Mail-Adresse und Telefonnummer von Absendern und Empfängern – auch aus dem Adressbuch der Plattform – nur als schlüsselabhängiger Hash (HMAC mit einem Schlüssel je Mandant) in die Kette ein; Postleitzahl und Land bleiben lesbar, sie tragen ohne den Rest keinen Personenbezug. Im Klartext stehen die Werte in den 90 Tagen in der Datenbank. Die Kette bleibt damit über Datenbank und Archiv hinweg prüfbar, und aus dem Archiv lässt sich für die gesamte Aufbewahrungsdauer belegen, wer wann eine Adresse geändert hat – der Wert selbst ist nach der Archivierung nur noch gegen eine bekannte Adresse verifizierbar, nicht mehr lesbar.
Export und Einsicht
- In der Plattform sehen Sie den Verlauf jeder Sendung, jedes Vorgangs, jedes Dokuments und jedes Vertrags – die fachlichen Ereignisse je Objekt.
- Das vollständige Protokoll Ihres Mandanten liefert die Schnittstelle; das Recht dazu haben Mandanten-Admins, Benutzer und Prüfer. Mandantenfremde Einträge sind dort nicht vorhanden, nicht nur ausgeblendet.
- Der Export (JSON) umfasst Datenbank und Archiv und enthält zu jedem Eintrag
entry_hash,prev_hashund die Version des Kettenverfahrens. Ihre Administratoren lösen ihn selbst aus – ebenso die Integritätsprüfung.