Za pierwszym razem, gdy audytor poprosi o Twój ślad audytowy, prawdopodobnie wyeksportujesz CSV z jakiegokolwiek systemu logowania, który już masz, i będzie wyglądał dobrze. Znaczniki czasu, nazwy użytkowników, akcje, identyfikatory zasobów, wszystko po kolei. Wtedy ktoś zada jedyne pytanie, które się liczy: skąd wiemy, że ten plik odpowiada temu, co naprawdę się wydarzyło? Niezmienny dziennik audytowy, którego nikt spoza Twojego zespołu nie może niezależnie sprawdzić, nie jest dowodem. To twierdzenie o Twoim własnym zachowaniu, wyprodukowane przez systemy, które kontrolujesz, w formacie, który sam wybrałeś. Audytorzy są wobec tego uprzejmi. Regulatorzy coraz mniej.
„Niezmienny” w większości stacków infrastrukturalnych oznacza coś słabszego, niż sugeruje to słowo. Zwykle oznacza append-only z konwencji - aplikacja tylko wstawia dane, a wszyscy zgadzają się, że nie uruchamiają UPDATE. Czasem oznacza politykę retencji na object store albo bucket typu WORM, który chroni przed usunięciem, ale nic nie mówi o tym, co zostało zapisane na starcie. Wszystko to są realne kontrole. Żadna z nich nie pozwala stronie trzeciej wykryć zmianę po fakcie. Ta luka - między „nikt nie powinien tego edytować” a „możesz udowodnić, że nikt tego nie zrobił” - to cały temat tego tekstu.
Jak działa ślad audytowy odporny na manipulację
Weź każde pole wpisu w logu, które ma znaczenie - akcję, kto ją wykonał, dla jakiej organizacji działał, zasób, zmienione wartości, correlation ID, poziom ryzyka, adres klienta - zserializuj je w ustalonej, uzgodnionej kolejności, dołącz hash poprzedniego wpisu i przelicz SHA-256 nad wynikiem. Zapisz ten skrót w wierszu jako entry_hash, a skrót poprzednika jako prev_hash. Pierwszy wiersz nie ma poprzednika, więc jego prev_hash jest null. Wszystko po nim jest przyspawane do tego, co było wcześniej.
Konsekwencja to część użyteczna. Zmień jeden znak we wpisie sprzed trzech miesięcy, a przeliczony hash przestaje zgadzać się z tym zapisanym w wierszu. Usuń wiersz w całości, a prev_hash następnego wiersza wskazuje na poprzednika, którego już nie ma. Wstaw sfabrykowany wpis, a nie będzie miał żadnego prawidłowego miejsca w sekwencji. Nie musisz ufać warstwie storage, operatorowi ani dostawcy. Potrzebujesz wpisów, reguł hashowania i skryptu. To właśnie czyni to śladem audytowym odpornym na manipulację, a nie po prostu uporządkowanym logiem.
Bądźmy precyzyjni co do tego, czego to nie daje. Łańcuch hashy jest odporny na manipulację (tamper-evident), a nie odporny na sfałszowanie w ogóle (tamper-proof). Atakujący z dostępem zapisu do bazy danych, znający reguły hashowania, może przepisać jeden wpis, a potem przeliczyć każdy hash po nim, produkując łańcuch, który weryfikuje się bez zarzutu. Pokonuje to publikowanie głowy łańcucha gdzieś, nad czym atakujący nie ma kontroli: podpisanie aktualnego entry_hash kluczem trzymanym poza bazą danych, wyeksportowanie go, wysłanie do audytora, zapisanie w osobnym systemie. Każde z tych działań zamraża historię do tego momentu.
Dlaczego jeden writer liczy się bardziej, niż myślisz
Łańcuch hashy ma dokładnie jedną głowę, a każdy nowy wpis musi ją odczytać, zanim się do niej podłączy. Uruchom dwóch writerów przeciw tej samej tabeli, a wpadną w wyścig - obaj czytają tę samą głowę, obaj się do niej podłączają, i jeden z nich jest już błędny. Łańcuch zgłasza sam siebie jako uszkodzony, a Ty spędzasz dzień szukając atakującego, który nie istnieje. Traktujemy to jako niezmiennik, a nie preferencję: jeden proces konsumuje kolejkę audytową i zapisuje tabelę, a włączenie drugiego writera to świadomy cutover, nie pokrętło do skalowania.
Błędy weryfikacji też muszą być czytelne, bo operatorzy reagują na pierwsze słowo, które przeczytają. Nasz weryfikator zgłasza trzy odrębne przyczyny. chain_broken oznacza, że łączenie jest błędne - że prev_hash nie zgadza się z poprzednikiem. content_tampered oznacza, że łączenie jest nienaruszone, ale przeliczenie hashu z zapisanych pól daje coś innego. entry_never_hashed oznacza, że wiersz został zapisany w ogóle bez hashu. Tylko drugi przypadek jest dowodem manipulacji, a zlanie trzech przyczyn w jeden komunikat to prosta droga do wywołania incydentu bezpieczeństwa z powodu buga w teście.
Ten trzeci przypadek nie jest hipotetyczny i warto się do niego przyznać. Garść wierszy w naszym własnym łańcuchu została zapisana przez test, który wstawiał dane bezpośrednio do tabeli surowym SQL-em, omijając writera, zostawiając obie kolumny hashy puste. Triggery bazy danych, które czynią tabelę append-only, odrzucają zarówno delete, jak i update, co oznacza, że te wiersze są trwale nie do naprawienia. Będą siedzieć w łańcuchu na zawsze, a każdy przebieg weryfikacji do nich dociera. Poprawką był helper, którego testy są zobowiązane używać, plus test-strażnik, który się wywala w chwili, gdy ktoś znów sięgnie po surowy SQL.
Co audytor powinien faktycznie dostać
Dostarczany materiał to nie zrzut ekranu z dashboardu z napisem „chain valid”. To CSV zawierający same wpisy, z obiema kolumnami hashy włącznie - id, akcja, aktor, organizacja, zakres klienta, typ i ID zasobu, poziom ryzyka, correlation ID, adres, user agent, prev_hash, entry_hash i znacznik czasu - plus pisemna specyfikacja dokładnie tego, które pola wchodzą do skrótu i w jakiej kolejności. Mając te dwie rzeczy, audytor pisze dwadzieścia linijek Pythona i sprawdza Twoją pracę, nie pytając Cię o nic więcej. To jest dowód: coś, co możesz przekazać, a potem stracić nad tym kontrolę.
Drobny szczegół o realnych konsekwencjach: wyeksportowane pola audytowe to ciągi znaków, na które może wpłynąć atakujący, a arkusze kalkulacyjne wykonają wszystko, co zaczyna się od znaku równości, plusa, minusa lub małpy. Eksport audytowy, który otwiera powłokę na laptopie audytora, to zapadający w pamięć sposób na oblanie audytu. Każde pole w naszym eksporcie jest escapowane, a wartości zaczynające się jak formuła są neutralizowane, zanim trafią do pliku. To rodzaj kontroli, która wygląda na przesadną drobiazgowość - dopóki pierwsza osoba nie otworzy paczki dowodów w Excelu.
Pod tym wszystkim sama tabela musi odmawiać niebezpiecznych operacji. Append-only wymuszone w kodzie aplikacji to obietnica, która przetrwa dokładnie tak długo, dopóki nikt nie napisze migracji, skryptu czyszczącego albo dobrze pomyślanej poprawki o drugiej w nocy. Triggery bazy danych, które odrzucają UPDATE i DELETE na tabeli audytowej, zamieniają tę obietnicę w twarde ograniczenie. Oznacza to też, że aplikacja i storage mogą się nie zgadzać tylko w jednym kierunku: baza danych może odmówić zapisu, którego chciała aplikacja, ale aplikacja nigdy po cichu nie przepisze bazy danych.
Weryfikuj swój niezmienny dziennik audytowy według harmonogramu
Większość zespołów, które mają endpoint weryfikujący, wywołuje go raz - podczas demo. To zły rytm. Przerwa w łańcuchu odkryta wtedy, gdy pyta o nią audytor, to problem śledczy obejmujący tyle miesięcy, ile minęło; ta sama przerwa odkryta przez zaplanowany przebieg to zgłoszenie buga ze znacznikiem czasu. Weryfikacja dużej tabeli musi być do tego zbudowana. Nasza przechodzi przez wpisy w uporządkowanych stronach, zamiast ładować wszystko do pamięci naraz, bo tabela rośnie z każdą mutacją na platformie, a weryfikator, który działa tylko na małych tabelach, to weryfikator, który przestanie działać.
Drugi nawyk to pokrycie. Łatwo jest umieścić logowanie audytowe w middleware HTTP, patrzeć, jak każda mutacja API pojawia się w śladzie, i uznać sprawę za zamkniętą. Wtedy zadanie cron archiwizuje dane klienta, konsument kolejki odwołuje agenta, worker w tle rotuje poświadczenie, i nic z tego nie jest w śladzie, bo nic z tego nie było requestem HTTP. Te ścieżki muszą logować jawnie. Niewygodna prawda jest taka, że akcje najmniej skłonne do zalogowania to dokładnie te, które śledczy najbardziej chce zobaczyć.
NIS2 i DORA pchają w tym samym kierunku: kontrole, które możesz zademonstrować, a nie tylko zadeklarować. Nikt nie da Ci certyfikatu za użycie SHA-256 i my też sobie takiego nie przypisujemy. Ale kiedy ktoś zapyta, kto zmienił regułę firewalla trzeciego marca, jest ogromna różnica między wynikiem zapytania a wynikiem zapytania, którego integralność może sprawdzić ktoś obcy. Trzy pytania warte zadania każdemu, kto sprzedaje Ci niezmienny dziennik audytowy: czy mogę wyeksportować hashe, czy mogę je przeliczyć bez waszego oprogramowania, i co się dzieje, gdy łańcuch zgłasza sam siebie jako uszkodzony?