Første gang en revisor ber om revisjonssporet ditt, eksporterer du sannsynligvis en CSV fra det loggsystemet du allerede kjører, og det ser greit ut. Tidsstempler, brukernavn, handlinger, ressurs-ID-er, alt i rekkefølge. Så spør noen det eneste spørsmålet som betyr noe: hvordan vet vi at denne filen stemmer med det som faktisk skjedde? En uforanderlig revisjonslogg som ingen utenfor teamet ditt kan sjekke uavhengig, er ikke bevis. Det er en påstand om din egen atferd, produsert av systemer du kontrollerer, i et format du selv valgte. Revisorer er høflige om dette. Tilsynsmyndighetene blir mindre det.

«Uforanderlig» betyr i de fleste infrastruktur-stacker noe svakere enn ordet antyder. Som regel betyr det append-only av konvensjon - applikasjonen setter bare inn nye rader, og alle er enige om å ikke kjøre en UPDATE. Noen ganger betyr det en oppbevaringspolicy på en objektlagring, eller en WORM-bøtte, som beskytter mot sletting, men sier ingenting om hva som ble skrevet i utgangspunktet. Alt dette er reelle kontroller. Ingen av dem lar en tredjepart oppdage en endring i etterkant. Det gapet - mellom «ingen skal kunne redigere dette» og «du kan bevise at ingen gjorde det» - er hele saken.

Hvordan et manipulasjonssikkert revisjonsspor fungerer

Ta hvert felt i en loggoppføring som betyr noe - handlingen, hvem som gjorde den, hvilken organisasjon de handlet på vegne av, ressursen, de endrede verdiene, korrelasjons-ID-en, risikonivået, klientadressen - serialiser dem i en fast, avtalt rekkefølge, legg til forrige oppførings hash, og kjør SHA-256 over resultatet. Lagre den digesten på raden som entry_hash, og lagre forgjengerens digest som prev_hash. Den første raden har ingen forgjenger, så dens prev_hash er null. Alt etter den er sveiset fast til det som kom før.

Konsekvensen er det nyttige. Endre ett eneste tegn i en tre måneder gammel oppføring, og den utregnede hashen matcher ikke lenger den som er lagret på raden. Slett en rad helt, og neste rads prev_hash peker på en forgjenger som ikke finnes. Sett inn en oppdiktet oppføring, og den har ingen gyldig plass i rekkefølgen i det hele tatt. Du trenger ikke stole på lagringslaget, operatøren eller leverandøren. Du trenger oppføringene, hash-reglene og et skript. Det er det som gjør det til et manipulasjonssikkert revisjonsspor fremfor en ryddig logg.

Vær presis på hva dette ikke gir deg. En hash-kjede er manipulasjonssikker i betydningen synlig, ikke manipulasjonssikker i betydningen umulig. En angriper som har skrivetilgang til databasen og kjenner hash-reglene, kan skrive om en oppføring og deretter regne ut hver hash etter den på nytt, og produsere en kjede som verifiserer perfekt. Det som stopper det er å publisere kjedens toppunkt et sted angriperen ikke kontrollerer: signere gjeldende entry_hash med en nøkkel holdt utenfor databasen, eksportere den, sende den til en revisor, skrive den inn i et separat system. Hver eneste av disse fryser historien frem til det tidspunktet.

Hvorfor én enkelt skriver betyr mer enn du tror

En hash-kjede har nøyaktig ett toppunkt, og hver ny oppføring må lese det før den kan lenke til det. Kjør to skrivere mot samme tabell, og de vil kappløpe - begge leser det samme toppunktet, begge lenker til det, og en av dem er nå feil. Kjeden rapporterer seg selv som ødelagt, og du bruker en dag på å lete etter en angriper som ikke finnes. Vi behandler dette som en invariant fremfor en preferanse: én prosess konsumerer revisjonskøen og skriver til tabellen, og å slå på en andre skriver er en bevisst cutover, ikke en skaleringsbryter.

Verifiseringsfeil må også være lesbare, fordi operatører reagerer på det første ordet de leser. Vår verifikator rapporterer tre distinkte årsaker. chain_broken betyr at lenkingen er feil, at prev_hash ikke matcher forgjengeren. content_tampered betyr at lenkingen er intakt, men at det å regne ut hashen på nytt fra de lagrede feltene gir noe annet. entry_never_hashed betyr at raden ble skrevet uten hash i det hele tatt. Bare den andre er bevis på manipulasjon, og å slå de tre sammen til én melding er slik man starter en sikkerhetshendelse over en bug i en test.

Det tredje tilfellet er ikke hypotetisk, og det er verdt å fortelle på oss selv. En håndfull rader i vår egen kjede ble skrevet av en test som satte inn direkte i tabellen med rå SQL, forbi skriveren, og lot begge hash-kolonnene stå tomme. Databasetriggerne som gjør tabellen append-only, avviser sletting like mye som oppdatering, noe som betyr at disse radene er permanent uopprettelige. De vil ligge i kjeden for alltid, og hver verifiseringskjøring når dem. Løsningen var en hjelpefunksjon som tester må bruke, pluss en vakt-test som feiler i det øyeblikket noen griper etter rå SQL igjen.

Hva en revisor faktisk bør motta

Leveransen er ikke et skjermbilde av et dashbord som sier «kjede gyldig». Det er en CSV med oppføringene selv med begge hash-kolonnene inkludert - id, handling, aktør, organisasjon, kundeomfang, ressurstype og -ID, risikonivå, korrelasjons-ID, adresse, brukeragent, prev_hash, entry_hash og tidsstempel - pluss en skriftlig spesifikasjon av nøyaktig hvilke felt som inngår i digesten og i hvilken rekkefølge. Med disse to tingene skriver en revisor tjue linjer Python og sjekker arbeidet ditt uten å be deg om noe mer. Det er bevis: noe du kan overlevere og deretter miste kontrollen over.

En liten detalj med reelle konsekvenser: eksporterte revisjonsfelt er strenger påvirket av angriperen, og regneark vil kjøre alt som starter med et likhetstegn, pluss, minus eller krøllalfa. En revisjonseksport som åpner et shell på revisorens bærbare er en minneverdig måte å mislykkes på en revisjon. Hvert felt i vår eksport blir escapet, og formel-prefiksede verdier nøytraliseres før de når filen. Det er den typen kontroll som virker pirkete helt til den første personen åpner bevispakken i Excel.

Under alt dette må selve tabellen nekte de farlige operasjonene. Append-only håndhevet i applikasjonskode er et løfte som overlever nøyaktig så lenge ingen skriver en migrering, et opprydningsskript eller en velmenende fiks klokken to om natten. Databasetriggere som avviser UPDATE og DELETE på revisjonstabellen gjør det løftet til en begrensning. Det betyr også at applikasjonen og lagringen bare kan være uenige i én retning: databasen kan avvise en skrivning applikasjonen ønsket, men applikasjonen kan aldri stille skrive om databasen.

Verifiser den uforanderlige revisjonsloggen på en fast plan

De fleste team med et verifiseringsendepunkt kaller det én gang, under demoen. Det er feil hyppighet. Et kjedebrudd oppdaget når revisoren spør er et rettsmedisinsk problem som dekker uansett hvor mange måneder som har gått; det samme bruddet oppdaget av en planlagt kjøring er en feilrapport med tidsstempel. Verifisering over en stor tabell må bygges for det. Vår går gjennom oppføringene i ordnede sider fremfor å laste alt inn i minnet, fordi tabellen vokser med hver mutasjon på plattformen, og en verifikator som bare fungerer på små tabeller er en verifikator som vil slutte å fungere.

Den andre vanen er dekning. Det er lett å legge revisjonslogging i HTTP-middlewaren, se hver API-mutasjon dukke opp i sporet, og erklære det ferdig. Så arkiverer en cron-jobb en kundes data, en køkonsument tilbakekaller en agent, en bakgrunnsjobb roterer en credential, og ingenting av det er i sporet, fordi ingenting av det var en HTTP-forespørsel. De stiene må logge eksplisitt. Den ubehagelige sannheten er at handlingene som er minst sannsynlig å bli logget, er nøyaktig de en gransker mest vil se.

NIS2 og DORA drar i samme retning: kontroller du kan demonstrere fremfor å hevde. Ingen kommer til å gi deg et sertifikat for å bruke SHA-256, og vi hevder ikke det heller. Men når noen spør hvem som endret brannmurregelen den tredje mars, er det stor forskjell på et spørringsresultat og et spørringsresultat hvis integritet en fremmed kan sjekke. Tre spørsmål verdt å stille alle som selger deg en uforanderlig revisjonslogg: kan jeg eksportere hashene, kan jeg regne dem ut på nytt uten programvaren deres, og hva skjer når kjeden rapporterer seg selv som ødelagt?