Коли аудитор вперше попросить у вас журнал аудиту, ви, ймовірно, експортуєте CSV із тієї системи логування, яку вже маєте, і на перший погляд усе буде виглядати добре. Мітки часу, імена користувачів, дії, ID ресурсів - усе по порядку. А потім хтось задає єдине питання, яке насправді має значення: звідки нам знати, що цей файл відповідає тому, що сталося насправді? Незмінний журнал аудиту, який ніхто поза межами вашої команди не може перевірити незалежно, - це не доказ. Це твердження про вашу власну поведінку, вироблене системами, які контролюєте ви, у форматі, який обрали ви. Аудитори поки що ставляться до цього ввічливо. Регулятори - все менше.
«Незмінний» у більшості інфраструктурних стеків означає щось слабше, ніж підказує саме слово. Зазвичай це означає append-only за домовленістю - застосунок тільки додає записи, і всі просто погоджуються не запускати UPDATE. Іноді це означає політику зберігання на object store або WORM-бакет, який захищає від видалення, але нічого не каже про те, що саме було записано спочатку. Усе це реальні контролі. Жоден з них не дозволяє третій стороні виявити зміну постфактум. Саме цей розрив - між «ніхто не повинен це редагувати» і «ви можете довести, що ніхто цього не робив» - і є темою цієї статті.
Як працює журнал аудиту з помітними ознаками втручання
Візьміть кожне важливе поле запису в журналі - дію, хто її виконав, від імені якої організації, ресурс, змінені значення, correlation ID, рівень ризику, адресу клієнта, - серіалізуйте їх у фіксованому, узгодженому порядку, додайте хеш попереднього запису і проженіть результат через SHA-256. Збережіть цей дайджест у рядку як entry_hash, а дайджест попередника - як prev_hash. У першого рядка немає попередника, тому його prev_hash дорівнює null. Усе, що йде після нього, приварене до того, що було перед ним.
Корисна частина - це наслідок. Змініть один символ у записі трьохмісячної давності - і перерахований хеш більше не збігатиметься зі збереженим у рядку. Видаліть рядок повністю - і prev_hash наступного рядка вказуватиме на попередника, якого там немає. Вставте сфабрикований запис - і він взагалі не матиме законного місця в послідовності. Вам не потрібно довіряти рівню зберігання, оператору чи вендору. Вам потрібні лише самі записи, правила хешування та скрипт. Саме це робить журнал слідом аудиту з помітними ознаками втручання, а не просто охайним логом.
Будьмо точними щодо того, чого це вам не дає. Хеш-ланцюг має помітні ознаки втручання, але не є захищеним від втручання в принципі. Зловмисник, який має доступ на запис до бази даних і знає правила хешування, може переписати запис, а потім перерахувати всі хеші після нього, отримавши ланцюг, який ідеально пройде перевірку. Що дійсно проти цього працює - публікація голови ланцюга там, де зловмисник її не контролює: підписання поточного entry_hash ключем, який зберігається поза базою даних, експорт його, надсилання аудитору, запис в окрему систему. Будь-що з цього заморожує історію до цього моменту.
Чому один процес запису важливіший, ніж здається
У хеш-ланцюга рівно одна голова, і кожен новий запис повинен прочитати її, перш ніж прив'язатися до неї. Запустіть два процеси запису проти однієї таблиці - і вони почнуть змагатися: обидва прочитають ту саму голову, обидва прив'яжуться до неї, і один із записів виявиться неправильним. Ланцюг повідомить, що він зламаний, і ви витратите день на пошуки зловмисника, якого не існує. Ми ставимося до цього як до інваріанта, а не як до переваги: один процес споживає чергу аудиту й пише в таблицю, а увімкнення другого процесу запису - це свідомий cutover, а не перемикач масштабування.
Помилки перевірки теж мають бути зрозумілими, бо оператори реагують на перше слово, яке бачать. Наш верифікатор повідомляє три окремі причини. chain_broken означає, що зв'язок неправильний - prev_hash не збігається з попередником. content_tampered означає, що зв'язок цілий, але перерахунок хешу зі збережених полів дає інший результат. entry_never_hashed означає, що рядок був записаний взагалі без хешу. Доказом втручання є лише другий варіант, і якщо звести всі три в одне повідомлення, ви отримаєте security-інцидент через баг у тесті.
Цей третій випадок не гіпотетичний, і варто розповісти про це на своєму прикладі. Кілька рядків у нашому власному ланцюгу були записані тестом, який вставляв дані напряму в таблицю сирим SQL, оминаючи writer, залишаючи обидві колонки з хешем порожніми. Тригери бази даних, які роблять таблицю append-only, відхиляють як видалення, так і оновлення, а це означає, що ці рядки назавжди залишаться непоправними. Вони сидітимуть у ланцюгу вічно, і кожен запуск перевірки на них натрапляє. Виправленням став хелпер, який тести зобов'язані використовувати, плюс guard-тест, що падає в ту ж мить, коли хтось знову тягнеться до сирого SQL.
Що аудитор має отримувати насправді
Результатом має бути не скріншот dashboard'а з написом «chain valid». Це CSV із самими записами, включно з обома колонками хешів - id, дія, actor, організація, scope клієнта, тип та ID ресурсу, рівень ризику, correlation ID, адреса, user agent, prev_hash, entry_hash і мітка часу - плюс письмова специфікація того, які саме поля йдуть у дайджест і в якому порядку. З цими двома речами аудитор пише двадцять рядків Python і перевіряє вашу роботу, більше нічого у вас не питаючи. Ось що таке доказ: те, що можна віддати і після цього втратити над цим контроль.
Маленька деталь із реальними наслідками: поля в експорті аудиту - це рядки, на вміст яких може впливати зловмисник, а електронні таблиці виконають будь-що, що починається зі знаку рівності, плюса, мінуса чи «собачки». Експорт аудиту, який відкриває shell на ноутбуці аудитора, - це запам'ятовуваний спосіб провалити аудит. Кожне поле в нашому експорті екранується, а значення з префіксом формули нейтралізуються ще до того, як потраплять у файл. Це той тип контролю, який виглядає прискіпливістю рівно до моменту, коли перша людина відкриває пакет доказів в Excel.
Під усім цим сама таблиця має відмовляти в небезпечних операціях. Append-only, забезпечений лише кодом застосунку, - це обіцянка, яка живе рівно доти, доки ніхто не напише міграцію, скрипт очищення чи благонамірений фікс о другій ночі. Тригери бази даних, які відхиляють UPDATE та DELETE на таблиці аудиту, перетворюють цю обіцянку на обмеження. Це також означає, що застосунок і сховище можуть розходитись лише в одному напрямку: база даних може відмовити в записі, якого хотів застосунок, але застосунок ніколи тихенько не перепише базу даних.
Перевіряйте незмінний журнал аудиту за розкладом
Більшість команд, у яких є verify-ендпоінт, викликають його один раз - під час демо. Це неправильна періодичність. Розрив ланцюга, виявлений тоді, коли про нього питає аудитор, - це forensic-проблема, що охоплює стільки місяців, скільки минуло; той самий розрив, виявлений плановим запуском, - це баг-репорт із міткою часу. Перевірка великої таблиці має бути побудована саме під це. Наша проходить записи впорядкованими сторінками, а не завантажує все в пам'ять одразу, бо таблиця росте з кожною мутацією на платформі, а верифікатор, який працює лише на маленьких таблицях, - це верифікатор, який рано чи пізно перестане працювати.
Друга звичка - це покриття. Легко покласти логування аудиту в HTTP middleware, побачити, як кожна мутація API з'являється в журналі, і оголосити справу закритою. А потім cron-задача архівує дані клієнта, consumer черги відкликає агента, фоновий worker ротує обліковий запис - і нічого з цього немає в журналі, бо жодна з цих дій не була HTTP-запитом. Ці шляхи мають логувати явно. Незручна правда в тому, що дії, які з найменшою ймовірністю потраплять у журнал, - це саме ті, які найбільше хоче побачити слідчий.
NIS2 та DORA тиснуть в одному напрямку: контролі, які можна продемонструвати, а не просто задекларувати. Ніхто не видасть вам сертифікат за використання SHA-256, і ми на нього не претендуємо. Але коли хтось питає, хто змінив правило файрволу третього березня, є велика різниця між результатом запиту і результатом запиту, цілісність якого може перевірити стороння людина. Три питання, які варто поставити будь-кому, хто продає вам незмінний журнал аудиту: чи можу я експортувати хеші, чи можу я перерахувати їх без вашого програмного забезпечення, і що відбувається, коли ланцюг повідомляє, що він зламаний?