Första gången en revisor ber om ditt granskningsspår kommer du förmodligen att exportera en CSV från vilket loggsystem du än redan kör, och det kommer att se bra ut. Tidsstämplar, användarnamn, åtgärder, resurs-ID:n, allt i ordning. Sedan ställer någon den enda frågan som spelar roll: hur vet vi att den här filen stämmer med vad som faktiskt hände? En oföränderlig granskningslogg som ingen utanför ditt team kan kontrollera oberoende är inte bevis. Det är ett påstående om ditt eget beteende, producerat av system du själv kontrollerar, i ett format du själv valde. Revisorer är artiga om det här. Tillsynsmyndigheter blir det allt mindre.
"Oföränderlig" betyder i de flesta infrastrukturstackar något svagare än vad ordet antyder. Det betyder oftast append-only av konvention - applikationen bara infogar, och alla är överens om att aldrig köra en UPDATE. Ibland betyder det en lagringspolicy på en objektlagring, eller en WORM-bucket, som skyddar mot radering men inte säger något om vad som skrevs från början. Allt det här är riktiga kontroller. Ingen av dem låter en tredje part upptäcka en ändring i efterhand. Det gapet - mellan ingen ska redigera det här och du kan bevisa att ingen gjorde det - är hela ämnet.
Hur ett manipulationsavslöjande granskningsspår fungerar
Ta varje fält i en loggpost som spelar roll - åtgärden, vem som gjorde den, vilken organisation de agerade för, resursen, de ändrade värdena, korrelations-ID:t, risknivån, klientadressen - serialisera dem i en fast, överenskommen ordning, lägg till föregående posts hash, och kör SHA-256 över resultatet. Spara den digesten på raden som entry_hash, och spara föregångarens digest som prev_hash. Den första raden har ingen föregångare, så dess prev_hash är null. Allt efter den är hopsvetsat med det som kom före.
Konsekvensen är den användbara delen. Ändra ett enda tecken i en tre månader gammal post, och dess omräknade hash matchar inte längre den som är sparad på raden. Radera en rad helt, och nästa rads prev_hash pekar på en föregångare som inte finns. Infoga en fabricerad post, och den har ingen giltig plats i sekvensen alls. Du behöver inte lita på lagringslagret, operatören eller leverantören. Du behöver posterna, hash-reglerna och ett skript. Det är det som gör det till ett manipulationsavslöjande granskningsspår snarare än en välstädad logg.
Var precis om vad det här inte ger dig. En hashkedja är manipulationsavslöjande, inte manipulationssäker. En angripare som har skrivåtkomst till databasen och känner till hash-reglerna kan skriva om en post och sedan räkna om varje hash efter den, och producera en kedja som verifierar perfekt. Det som slår ut det är att publicera kedjans huvud någonstans angriparen inte kontrollerar: signera den aktuella entry_hash med en nyckel som förvaras utanför databasen, exportera den, skicka den till en revisor, skriva in den i ett separat system. Vilket som helst av det fryser historiken fram till den stunden.
Varför en enda skrivprocess spelar större roll än du tror
En hashkedja har exakt ett huvud, och varje ny post måste läsa det innan den kan länka till det. Kör två skrivprocesser mot samma tabell, och de kommer att kapplöpa - båda läser samma huvud, båda länkar till det, och en av dem har nu fel. Kedjan rapporterar sig själv som trasig, och du lägger en dag på att leta efter en angripare som inte finns. Vi behandlar det här som en invariant snarare än en preferens: en process konsumerar granskningskön och skriver till tabellen, och att slå på en andra skrivprocess är en medveten omställning, inte en skalningsreglage.
Verifieringsfel måste också vara begripliga, för operatörer reagerar på det första ordet de läser. Vår verifierare rapporterar tre distinkta orsaker. chain_broken betyder att länkningen är fel, att prev_hash inte matchar föregångaren. content_tampered betyder att länkningen är intakt men att omräkning av hashen från de sparade fälten ger något annat. entry_never_hashed betyder att raden skrevs utan hash överhuvudtaget. Bara den andra är bevis på manipulation, och att slå ihop alla tre till ett enda meddelande är så man startar en säkerhetsincident på grund av en bugg i ett test.
Det tredje fallet är inte hypotetiskt, och det är värt att berätta om oss själva. En handfull rader i vår egen kedja skrevs av ett test som infogade direkt i tabellen med rå SQL, förbi skrivprocessen, och lämnade båda hash-kolumnerna tomma. Databastriggrarna som gör tabellen append-only avvisar både raderingar och uppdateringar, vilket betyder att de raderna är permanent oreparerbara. De kommer att ligga kvar i kedjan för alltid, och varje verifieringskörning når fram till dem. Fixen var en hjälpfunktion som tester är skyldiga att använda, plus ett skyddstest som misslyckas i samma stund någon sträcker sig efter rå SQL igen.
Vad en revisor faktiskt bör få
Leveransen är inte en skärmdump av en dashboard som säger "kedjan giltig". Det är en CSV som innehåller posterna själva med båda hash-kolumnerna inkluderade - id, action, actor, organisation, customer scope, resource type och ID, risknivå, korrelations-ID, adress, user agent, prev_hash, entry_hash och tidsstämpel - plus en skriftlig specifikation över exakt vilka fält som ingår i digesten och i vilken ordning. Med de här två sakerna skriver en revisor tjugo rader Python och kontrollerar ditt arbete utan att behöva fråga dig om något mer. Det är bevis: något du kan lämna över och sedan förlora kontrollen över.
En liten detalj med verkliga konsekvenser: exporterade granskningsfält är strängar en angripare kan påverka, och kalkylblad kör allt som börjar med likhetstecken, plus, minus eller at-tecken. En granskningsexport som öppnar ett skal på revisorns laptop är ett minnesvärt sätt att underkännas i en granskning. Varje fält i vår export är escapat, och formelprefixade värden neutraliseras innan de når filen. Det är den typen av kontroll som ser ut som petighet ända fram till att den första personen öppnar bevispaketet i Excel.
Under allt det här måste själva tabellen vägra de farliga operationerna. Append-only som upprätthålls i applikationskod är ett löfte som håller exakt så länge ingen skriver en migrering, ett städskript eller en välmenande fix klockan två på natten. Databastriggrar som avvisar UPDATE och DELETE på granskningstabellen gör om det löftet till en begränsning. Det betyder också att applikationen och lagringen bara kan vara oense åt ett håll: databasen får vägra en skrivning applikationen ville göra, men applikationen kan aldrig i tysthet skriva om databasen.
Verifiera din oföränderliga granskningslogg enligt schema
De flesta team som har en verifieringsendpoint anropar den en gång, under demot. Det är fel takt. Ett kedjebrott som upptäcks när revisorn frågar är ett forensiskt problem som täcker hur många månader det än har gått; samma brott upptäckt av en schemalagd körning är en buggrapport med en tidsstämpel. Verifiering över en stor tabell måste vara byggd för det. Vår går igenom posterna i ordnade sidor istället för att läsa in allt i minnet, för tabellen växer med varje mutation på plattformen, och en verifierare som bara fungerar på små tabeller är en verifierare som kommer sluta fungera.
Den andra vanan är täckning. Det är lätt att lägga granskningsloggning i HTTP-mellanvaran, se varje API-mutation dyka upp i spåret, och förklara det klart. Sedan arkiverar ett cron-jobb en kunds data, en köanvändare återkallar en agent, en bakgrundsprocess roterar en autentiseringsuppgift, och inget av det hamnar i spåret, eftersom inget av det var en HTTP-förfrågan. De vägarna måste logga explicit. Den obekväma sanningen är att de åtgärder som är minst sannolika att loggas är precis de en utredare mest vill se.
NIS2 och DORA drar åt samma håll: kontroller du kan visa upp snarare än bara påstå. Ingen kommer att ge dig ett certifikat för att använda SHA-256, och vi påstår inte att vi har ett. Men när någon frågar vem som ändrade brandväggsregeln den tredje mars finns det en stor skillnad mellan ett frågeresultat och ett frågeresultat vars integritet en främling kan kontrollera. Tre frågor värda att ställa till vem som helst som säljer dig en oföränderlig granskningslogg: kan jag exportera hasharna, kan jag räkna om dem utan din programvara, och vad händer när kedjan rapporterar sig själv trasig?