# Аудиторские журналы с хеш-цепочкой: что они доказывают, а чего не могут

Хеш-цепочка даёт веское свидетельство того, что набор аудиторских записей не редактировали, не переставляли и не вставляли незаметно в середину после создания цепочки. Но она не доказывает, что приложение записало каждое важное событие, что временная метка соответствует реальному времени или что действие выполнил названный человек. Если считать цепочку доказательством всего этого, можно собрать впечатляющий пакет свидетельств, который рассыплется при первом серьёзном вопросе.

Я видел, как разборы инцидентов останавливались, потому что люди просили журнал ответить на вопросы, для которых он не предназначен. Цепочка успешно проходила проверку, но никто не мог установить, записал ли сервис отклонённый запрос, была ли личность оператора настоящей и не отставали ли системные часы. Криптография работала. Пакет свидетельств был неполным.

Для разработчиков, которые запускают агентов с доступом к API и SSH, это различие особенно важно. Агент может быстро выполнить множество действий, а позже часто требуется точный ответ: какой процесс запросил этот вызов, с какими полномочиями, какой именно запрос отправил исполнитель, что вернулось и одобрил ли это кто-то? Хеш-цепочка защищает часть этой истории. Остальное она за вас не запишет.

## Действительная цепочка доказывает непрерывность записей, а не реальность

Действительная хеш-цепочка доказывает, что каждая доступная запись фиксирует связь с предыдущей и что последовательность оставалась внутренне согласованной с выбранной начальной точки. Если кто-то изменит старое событие, поменяет два события местами или вставит запись между двумя существующими, проверка завершится ошибкой. Исключение возможно лишь в случае, если злоумышленник сумеет пересчитать все последующие связи и заменить все доверенные контрольные точки.

Это важное свидетельство. Следователь может сказать: «Эти записи образуют ту же последовательность, которая привела к известной вершине цепочки». Формулировка имеет значение. Утверждение зависит от известной вершины цепочки или другой доверенной опоры. Если единственная копия цепочки и её итогового дайджеста находится на машине, которую контролировал злоумышленник, он мог переписать и то и другое.

Хеш-цепочка не доказывает следующее:

- Приложение заметило каждое событие, которое следовало записать.
- Содержимое события точно описывает то, что произошло вне регистратора.
- Время события совпадает с показаниями надёжных часов.
- Действие выполнил человек, а не скомпрометированный процесс, использовавший его учётные данные.
- Цепочка началась до того, как злоумышленник получил контроль над системой.

Это разные утверждения, и для каждого нужны свои свидетельства. Не называйте журнал «защищённым от подделки». Программа не получает такой статус только потому, что использует SHA-256. Цепочка делает изменения обнаруживаемыми при оговорённых предположениях. Это и есть полезное свойство, которое можно обосновать.

Особого внимания требует различие между **целостностью** и **полнотой**. Целостность отвечает на вопрос, изменяли ли имеющиеся записи. Полнота спрашивает, не пропущены ли записи. Цепочка хорошо отвечает на первый вопрос. На второй она отвечает только тогда, когда внешние свидетельства фиксируют ожидаемые контрольные точки и область событий, которые регистратор обязан был отправить.

## Формат записи определяет, что именно покрывает хеш

Цепочка защищает только те байты, которые попадают в дайджест. Прежде чем обсуждать алгоритмы, определите канонический формат события и включите все поля, которые понадобятся следователю для понимания действия.

Минимальная запись может выглядеть так:

```json
{
  "sequence": 1842,
  "event_id": "7b2ea6de-9c3f-4bb4-b1d7-8b13fbb1c5b9",
  "recorded_at": "2025-03-08T17:14:22.481Z",
  "actor": {"kind": "agent_process", "process_id": "p-91f"},
  "action": "http.request",
  "target": "api.example.internal/v1/releases",
  "request_digest": "sha256:...",
  "result": {"status": 201, "response_digest": "sha256:..."},
  "previous_hash": "sha256:..."
}
```

Записывающая сторона сериализует запись в одном определённом виде, хеширует эти байты и сохраняет полученный дайджест как ссылку на предшественника в следующей записи. Концептуально:

```text
record_hash[n] = SHA-256(canonical_record[n])
canonical_record[n+1].previous_hash = record_hash[n]
```

Поле `previous_hash` само должно входить в набор байтов, который покрывает `record_hash[n]`. Если его не включить, получится неловкая, но вполне реальная ошибка реализации. Записи будут выглядеть так, будто содержат дайджесты, но последовательность не будет связана.

Канонизация не мелочь. Два сериализатора JSON могут по-разному располагать поля объекта, экранировать Unicode или форматировать числа. Проверяющий, который заново собирает JSON вместо проверки исходных байтов, может отклонить честные записи или, что ещё хуже, создать разногласия о смысле записи. Храните исходное представление в байтах, укажите кодировку и проверяйте работу независимых реализаций.

Цепочка должна покрывать контекст, а не только тело запроса. Запись со словами «развёрнуто» даёт слабое свидетельство. Запись, связывающая версию исполнителя, тип действия, идентификатор цели, аутентифицированную личность процесса, решение об авторизации, дайджест запроса, дайджест ответа, порядковый номер и время записи, даёт проверяющему материал для оценки. Она всё равно не доказывает истинность каждого поля, зато не позволяет позднее менять историю по одному полю.

В специальной публикации NIST 800-92, *Guide to Computer Security Log Management*, сформулирована та же практическая мысль в более простых операционных терминах: записи журнала должны содержать достаточно сведений о событии, источнике, пользователе, статусе и времени для анализа, а организации должны защищать данные журналов. Идеальная цепочка вокруг расплывчатых записей идеально сохраняет расплывчатые записи. Это не проектирование аудита.

## Первая запись и отсутствующий хвост остаются уязвимыми

У каждой цепочки есть первая запись, которую часто называют исходной. Её значение предшественника фиксируется соглашением, например дайджестом пустой строки, или она ссылается на более раннюю контрольную точку. После этой точки цепочка может установить непрерывность. Но она не объясняет, почему именно эта точка считается началом истории.

Представьте сервис, который записывает записи с 1 по 10 000 в локальное хранилище. Злоумышленник получает полный контроль, удаляет записи с 1 по 7 000, меняет поле последовательности в оставшихся записях и создаёт новую цепочку, начиная с того, что раньше было записью 7 001. Поддельная цепочка проходит проверку. Проверяющий, у которого нет более ранней контрольной точки, не видит криптографического дефекта.

Та же проблема есть у хвоста. Сбой, отключение питания или злоумышленник могут помешать последним событиям попасть в надёжное хранилище. Последняя сохранённая запись может успешно пройти проверку, хотя через мгновение произошло действие. Цепочка доказывает, что сохранённый конец не изменяли задним числом. Она не доказывает, что это настоящее последнее событие.

Оба пробела уменьшаются, если выпускать контрольные точки вне контроля записывающей стороны. Контрольная точка должна содержать как минимум идентификатор цепочки, порядковый номер, хеш записи и время контрольной точки. Отправляйте её в отдельную учётную запись, хранилище с однократной записью, внешний сервис временных меток или сборщик под независимым управлением. Подписанная контрольная точка лучше неподписанной, потому что связывает утверждение с личностью, владеющей ключом подписи.

RFC 3161 описывает протокол временных меток, в котором центр временных меток подписывает свидетельство того, что заметил отпечаток сообщения в указанное время. Это поддерживает узкое, но полезное утверждение: к этому времени у центра был данный дайджест. Протокол не говорит, было ли содержимое события честным, не пропустили ли события до отправленной контрольной точки и имел ли исполнитель нужные полномочия. Используйте его для свидетельств времени и существования, а не вместо операционных записей.

Частота контрольных точек зависит от риска. Частые контрольные точки сокращают интервал, за который можно удалить незафиксированный хвост. Одновременно они создают больше внешних записей, которые нужно хранить и сверять. Не утверждайте, что ежечасные контрольные точки защищают реконструкцию инцидента с точностью до минуты. Укажите максимальный интервал без внешней опоры в рабочей процедуре.

## Хеш не превращает временную метку в доверенное время

Временная метка журнала показывает, что сказала clock регистратора, когда он создал запись. Для обычной отладки этого может быть достаточно. Но для вопросов о сроках, торговых окнах, доступе после увольнения или порядке событий на разных машинах это слабое свидетельство.

Администратор может изменить локальные часы. Виртуальная машина может возобновить работу с устаревшим временем. Синхронизация сетевого времени может отказать. Даже правильно синхронизированный узел фиксирует момент, когда его приложение записало событие, а он может отличаться от времени получения запроса удалённым сервисом или фиксации изменения.

Если это различие важно, храните времена отдельно:

- `observed_at`: когда исходный компонент заметил событие.
- `recorded_at`: когда записывающая сторона создала аудиторскую запись.
- `remote_at`: время, возвращённое удалённой системой, если она его предоставляет.
- `checkpoint_at`: когда независимый свидетель принял вершину цепочки.

Не сводите их в одно удобное поле `timestamp`. У каждого источника свой источник и свой способ отказа. Тогда следователь сможет рассуждать о диапазоне, а не полагаться на ложную точность.

Если нужны свидетельства времени, фиксируйте способ синхронизации времени на узле, сохраняйте предупреждения о состоянии синхронизации и храните квитанции подписанных контрольных точек. Для операций с высокими последствиями сравнивайте локальную запись с собственным аудиторским событием удалённого сервиса. Запрос, записанный в 10:02:01, и удалённое изменение, записанное в 10:02:05, могут описывать одно действие. Цепочка защищает вашу запись от редактирования, а сопоставление связывает её с внешней системой.

Часто рекомендуют: «Используйте базу данных только для добавления и временные метки, и проблема аудита решена». Совет популярен, потому что кажется простым на практике. Но он ошибочен: режим добавления внутри одного сервиса почти ничего не говорит о доверии к часам, пропущенных событиях, личности или внешних последствиях. Используйте хранилище только для добавления, если оно подходит, но назовите доказательства, которых всё ещё не хватает.

## Атрибуция требует отдельного следа личности

Хеш-цепочка может сохранить утверждение об исполнителе, например `actor = alice@example.com`. Но она не доказывает, что Алиса передала учётные данные, что поставщик идентификации правильно её аутентифицировал или что злоумышленник не воспользовался активной сессией. Дайджест защищает фразу, а не её истинность.

Для действий агентов личность процесса часто полезнее расплывчатого поля пользователя. Записывайте идентификатор процесса агента, родительский процесс, если он доступен, организацию, подписавшую исполняемый файл, время запуска, идентификатор сессии и человека или сервисную учётную запись, одобривших сессию. Одного имени процесса недостаточно. Любая программа может выбрать знакомое имя.

Подпись кода позволяет сделать ограниченное утверждение: операционная система может установить, что исполняемый файл содержит подпись, связанную с организацией, выполняющей подписание, и что эта подпись действительна по правилам платформы. Это не доказывает добрые намерения оператора программы. Но помогает отличить известную сборку от произвольного бинарного файла с тем же именем.

Для аутентификации и авторизации также нужны отдельные записи. Аутентификация показывает, какие учётные данные или субъект приняла система. Авторизация объясняет, почему система разрешила действие в тот момент. Свидетельством авторизации могут быть диалог подтверждения, назначение роли, область действия токена или заявка на изменение. Поместите в запись действия стабильную ссылку или дайджест этого решения, а исходную запись решения храните с контролем доступа.

Цифровые подписи усиливают этот слой, если использовать их правильно. Если записывающая сторона подписывает периодические вершины цепочки, проверяющий может установить, что подписи создал владелец закрытого ключа. Это добавляет утверждение об источнике по сравнению с нехешированными данными. Но всё по-прежнему зависит от защиты закрытого ключа, статуса сертификата, записей о ротации и связи субъекта сертификата с реальной операционной личностью.

Не прячьте всё это в одной строке `user`. Во время инцидента нужно различать человека, одобрившего запуск, процесс, запросивший действие, сервис, выполнивший его, и центр учётных данных, который его принял. Это могут быть четыре разных участника.

## Свидетельство авторизации отвечает на другой вопрос, чем запись активности

Запись активности отвечает на вопрос: «Что сделал исполнитель?» Запись авторизации отвечает на вопрос: «Почему ему разрешили это сделать?» Команды часто объединяют их, потому что обе появляются во время одного запроса. Такое объединение усложняет расследование.

Предположим, агент запрашивает SSH-команду. Исполнитель записывает целевой узел, дайджест команды, результат, личность процесса и время. Отдельно слой авторизации записывает, что новый процесс получил разрешение, кто его одобрил, что именно покрывало одобрение и когда оно истекло. Если SSH-команда запускается позже, событие активности должно ссылаться на соответствующую запись авторизации.

Такой дизайн позволяет проверяющему задавать правильные вопросы. Была ли команда отправлена? Проверьте запись активности. Была ли у сессии нужная разрешённая область? Проверьте запись авторизации. Показывал ли экран подтверждения настоящую личность? Проверьте записи интерфейса и личности процесса. Выполнил ли команду удалённый узел? Проверьте его серверные журналы или итоговое состояние.

Одобрение сессии и одобрение каждого использования конфиденциальных учётных данных дают разные свидетельства. Одобрение сессии подтверждает, что человек разрешил конкретному процессу работать в течение его жизненного цикла. Одобрение отдельного действия фиксирует более узкое решение, связанное с конкретной операцией. Ни один вариант не лучше автоматически. Выбор зависит от частоты действий, последствий и того, способен ли человек реально оценивать повторяющиеся запросы.

Усталость от подтверждений говорит о неудачном дизайне, а не служит причиной перестать их записывать. Если человек видит сотни одинаковых запросов, нажатия дают мало свидетельств об осознанной авторизации. Объединяйте низкорисковую работу в ограниченное решение для сессии, оставляйте повторное подтверждение для учётных данных или действий, где человек может оценить цель и последствия, и записывайте область действия простыми словами.

Само событие одобрения тоже не доказывает осознанное согласие. Оно доказывает, что механизм подтверждения записал решение. Вес этого решения зависит от деталей процесса, показанных пользователю, связи между этим экраном и выполняющим процессом, а также от аудиторской записи.

## Полнота зависит от того, где действия могут обойти регистратор

Нельзя получить полный журнал действий агента, просто добавив хеш-цепочку к файлу журнала после того, как агент уже получил исходные учётные данные. Получив API-токен или закрытый SSH-ключ, агент может вызвать другой клиент, скопировать секрет или отправить запрос в обход проверяемого пути. Цепочка может честно сохранить увиденные запросы и пропустить те, которые важнее всего.

Полнота начинается с контроля границы действия. Компонент, владеющий учётными данными, должен сам выполнить сетевой запрос или SSH-операцию и записать решение и результат до возврата ответа. Агент должен получить результат, а не секрет. Тогда утверждение меняется с «мы попросили агента записывать свою работу» на «владелец учётных данных заметил каждое использование через этот канал».

Даже в этом случае область действия нужно определить явно. Если разработчик может использовать те же учётные данные в терминале, журнал покрывает использование через агента, но не всё использование этих учётных данных. Если агент может выйти во вторую сеть в обход исполнителя, журнал не покрывает этот путь. Утверждение о полноте должно называть канал, субъекта и период, который оно охватывает.

Sallyport применяет такую границу для поддерживаемых HTTP- и SSH-каналов: зашифрованное хранилище держит API- и SSH-учётные данные вне агента, а приложение выполняет действие и записывает его. Это даёт аудиторскому журналу более чёткую область действия, чем журнал активности на стороне агента, но не позволяет делать утверждения о действиях через несвязанный путь к учётным данным.

Полезная проверка отказов начинается с карты всех путей от агента к внешнему эффекту. Учтите прямые HTTP-клиенты, оболочку, локальные файлы с токенами, сессии браузера, сервисы метаданных облака, переменные CI и конфигурацию SSH. Каждый путь вне зарегистрированного исполнителя является исключением из утверждения о полноте. Закройте его, изолируйте или честно зафиксируйте как исключение.

## Конфиденциальность, доступ и хранение требуют отдельных мер контроля

Сами хеш-цепочки ничего не раскрывают, но аудиторские записи часто содержат сведения, которые не стоит помещать в обычный журнал приложения. URL запросов могут раскрывать идентификаторы клиентов. Командные строки могут содержать секреты. Ответы могут включать персональные данные. Цепочка, затрудняющая изменение таких записей, одновременно может затруднить исправление неосторожного раскрытия.

Шифруйте аудиторское хранилище и ограничивайте круг читателей. По возможности разделяйте возможность проверить целостность и возможность расшифровать содержимое. Проверяющий может пересчитать хеши зашифрованных байтов и сравнить ссылки на предшественников, не видя защищённого содержимого. Это полезно, когда сначала нужно установить целостность закрытого архива, а уже потом передать его уполномоченному следователю.

Редактирование должно происходить до включения записи в цепочку. Замена секрета после записи изменит байты и нарушит цепочку. Вместо этого запишите дайджест конфиденциального тела запроса, стабильную ссылку на токен, длину, если она важна, или структурированное указание на то, что поле скрыто. Опишите правило редактирования. Иначе две похожие записи могли быть обработаны разной логикой.

Срок хранения тоже влияет на доказательную ценность. Если задача хранения удаляет старые записи, она должна создать собственную запись с диапазоном удалённых данных, правилом хранения, временем удаления и последней контрольной точкой перед удалением. Сохраните контрольную точку в отдельном архиве. Цепочка не вернёт удалённые данные, но сохранившееся свидетельство покажет, что удаление прошло по установленной процедуре, а не произошло незаметно.

Важен и журнал доступа. Записывайте, кто экспортировал архив аудита, кто расшифровал его и кто изменил настройки хранения. Во время инцидента аудиторские журналы привлекают внимание. Цепочка защищает прошлые записи от незаметного редактирования, но широкая группа читателей всё ещё может скопировать конфиденциальное содержимое или заставить оператора отключить будущую запись.

## Проверяйте архив до того, как он понадобится

Проверка должна быть обычной операцией с сохранённым результатом, а не командой, которую кто-то впервые запускает во время сбоя. Процедура должна указывать точный диапазон архива, версию проверяющего, ожидаемый источник контрольной точки, время проверки, оператора и результат.

Проверяющий должен обнаруживать как минимум четыре ошибки: некорректные записи, неправильную ссылку на предшественника, запись, чей дайджест не совпадает с сохранёнными байтами, и несоответствие вычисленной вершины ожидаемой контрольной точке. Результат должен быть удобно сохранять. Например:

```text
$ audit verify --archive agent-actions-2025-03-08.log --checkpoint checkpoints/2025-03-08.json
chain_id: agent-actions-prod-a
records_checked: 1842
first_sequence: 1
last_sequence: 1842
computed_head: sha256:4e7c...a912
checkpoint_head: sha256:4e7c...a912
result: VALID
```

Результат `VALID` означает, что проверяющий сравнил переданные байты с переданной контрольной точкой. Он не означает «присутствует каждое действие в рабочей среде» или «события истинны». Впишите это ограничение прямо в процедуру. Иначе под давлением люди придадут зелёному результату больше смысла, чем он имеет.

Проверяйте разрушительные сценарии на непроизводственном архиве. Измените один байт в теле записи. Переставьте две записи. Удалите запись из середины. Замените вершину контрольной точки. Обрежьте хвост. Проверяющий должен отклонить каждый случай, кроме обычного обрезания хвоста без ожидаемой конечной контрольной точки, когда он может сообщить о действительном префиксе. Такой результат корректен, и беспокоиться нужно по правильной причине.

Sallyport предоставляет офлайн-проверку цепочки через `sp audit verify`, а его проверяющий может проверить зашифрованную цепочку аудита без ключа хранилища. Запускайте эту проверку после экспорта и заранее отрабатывайте её сопоставление с отдельными представлениями сессий и активности, не дожидаясь расследования.

## Для пакета свидетельств нужны записи, которые продуктивно расходятся

Убедительное расследование опирается на несколько записей с разными способами отказа. Совпадение независимых источников весит больше, чем один журнал, который повторяет собственные предположения.

Для чувствительного действия агента сохраняйте запись активности с хеш-цепочкой, запись сессии или подтверждения, аутентифицированную личность процесса, соответствующую версию конфигурации, квитанцию внешней контрольной точки и собственную запись целевой системы или наблюдаемый результат. Для обычного запроса не нужны все источники. Но их должно быть достаточно с учётом последствий ошибки.

Ожидайте, что источники иногда будут расходиться. Локальная запись активности может предшествовать временной метке удалённого сервиса, потому что доставка по сети заняла время. Запрос может получить успешный ответ, а удалённая система позже откатит операцию. Запись сессии может показать одобрение, а запись действия не показать использования выданных полномочий. Это материал для расследования, а не дефекты, которые нужно сгладить.

Запишите точные утверждения, которые поддерживает ваша система. Например: «Этот архив содержит неизменённую последовательность записей исполнителя до контрольной точки X». Затем укажите утверждения, которые без других записей не поддерживаются: «Сам по себе этот архив не доказывает, что прямого использования учётных данных не было». Чёткие ограничения делают сильные утверждения убедительными.

Первое практическое действие: возьмите одну операцию агента с серьёзными последствиями и восстановите её от начала до конца. Определите границу действия, поля, записанные до выполнения, поля после выполнения, свидетельства одобрения, место контрольной точки, внешнее подтверждение и все пути обхода. Если какая-то часть зависит от того, что кто-то вспомнит произошедшее, у вас пока нет аудиторского следа. У вас есть история, которая может не выдержать столкновения с инцидентом.
