AUD-02: unveraenderliches-protokoll-append-only
Append-only per Trigger (nicht nur GRANT/REVOKE): audit_events_prevent_mutation() wirft bei jedem UPDATE/DELETE auf audit_events eine Exception, unabhaengig von der verbindenden Rolle (Akzeptanzkriterium 1). internal/audit/four_eyes.go: Vier-Augen-Prinzip fuer sicherheitskritische Entscheidungen (Loeschbestaetigung, Rechtevergabe), 1:1 nach archivdms- Vorbild. Request erzeugt einen Klartext-Code (wird ausserhalb des Systems an eine ZWEITE Person uebermittelt) und speichert nur dessen SHA-256-Hash. Confirm sperrt die Zeile mit FOR UPDATE (Akzeptanzkriterium 2 — serialisiert zwei gleichzeitige Bestaetigungsversuche, verhindert doppelte Ausfuehrung), weist eine Bestaetigung durch dieselbe Person wie die anfordernde ab (ErrSameActor, echtes Vier-Augen-Prinzip statt nur Code-Pruefung), und vergleicht den Code timing-safe (Akzeptanzkriterium 3). internal/audit/timingsafe.go: timingSafeEqual als projektweite Referenz- implementierung (subtle.ConstantTimeCompare) fuer sicherheitsrelevante Vergleiche — andere Module (z.B. Archive CMP-06 Freigabelinks) uebernehmen dasselbe Muster laut IAM-02-Konvention. Nebenbei behoben: AUD-01s eigener Test nutzte einen festen Tenant-Slug mit DELETE-basiertem Cleanup — seit dem neuen Append-only-Trigger schlaegt dieses Cleanup lautlos fehl, wodurch Zeilen sich ueber Testlaeufe hinweg summierten und die Zaehl-Assertion brach. Auf eindeutigen Slug pro Lauf umgestellt (direkte, notwendige Folge dieser Kachel, keine Umgestaltung von AUD-01 selbst). Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Direkter UPDATE/DELETE-Versuch von der Datenbank abgewiesen — TestAppendOnly_RejectsUpdateAndDelete: beide Operationen scheitern, Eintrag bleibt unveraendert erhalten. PASS. 2. Vier-Augen-Prinzip mit FOR-UPDATE-Lock race-frei unter parallelen Anfragen — TestFourEyes_ConcurrentConfirmIsRaceFree: zwei gleichzeitige Bestaetigungsversuche fuer denselben Vorgang, genau einer erfolgreich, der andere ErrAlreadyDecided. PASS. 3. Timing-safe Vergleich per Laufzeitmessung stichprobenartig verifiziert — TestTimingSafeEqual_NoEarlyExitTiming: Mismatch am Anfang (603µs) vs. am Ende (574µs) ueber 20000 Iterationen, kein Hinweis auf Short-Circuit- Vergleich (Ratio innerhalb Faktor 3 Toleranz). PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b12d53f469
commit
1bfb2efd94
@@ -0,0 +1,3 @@
|
||||
DROP TRIGGER IF EXISTS audit_events_no_delete ON audit_events;
|
||||
DROP TRIGGER IF EXISTS audit_events_no_update ON audit_events;
|
||||
DROP FUNCTION IF EXISTS audit_events_prevent_mutation();
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Audit-Log technisch gegen Aenderung/Loeschung absichern (AUD-02, siehe
|
||||
-- core-kanban/tickets/AUD-02.md). Ein Trigger statt nur GRANT/REVOKE, damit
|
||||
-- der Schutz unabhaengig davon greift, mit welcher Rolle verbunden wird
|
||||
-- (Akzeptanzkriterium 1: "auf Datenbankebene technisch unterbunden").
|
||||
CREATE FUNCTION audit_events_prevent_mutation() RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'audit_events ist append-only: % ist nicht erlaubt', TG_OP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER audit_events_no_update
|
||||
BEFORE UPDATE ON audit_events
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_events_prevent_mutation();
|
||||
|
||||
CREATE TRIGGER audit_events_no_delete
|
||||
BEFORE DELETE ON audit_events
|
||||
FOR EACH ROW EXECUTE FUNCTION audit_events_prevent_mutation();
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS security_confirmations;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Vier-Augen-Prinzip fuer sicherheitskritische Entscheidungen (AUD-02
|
||||
-- Akzeptanzkriterium 2), Vorbild: archivdms FOR-UPDATE-Lock + Timing-safe
|
||||
-- Vergleich. code_hash speichert NIEMALS den Bestaetigungscode im Klartext.
|
||||
CREATE TABLE security_confirmations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
action TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
requested_by TEXT NOT NULL,
|
||||
code_hash BYTEA NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'rejected')),
|
||||
confirmed_by TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
confirmed_at TIMESTAMPTZ
|
||||
);
|
||||
Reference in New Issue
Block a user