MAATRIX / Блог / Как хранить логи так, чтобы их нельзя было переписать

Как хранить логи так, чтобы их нельзя было переписать

MAATRIX

Первое, что делает более-менее аккуратный атакующий после получения root — чистит /var/log, правит .bash_history и вычищает журнал auditd. Если ваши логи лежат только на этом же сервере, то расследование инцидента превращается в гадание: вы точно знаете, что что-то произошло, но не можете сказать что, когда и как глубоко. Ниже — рабочая схема, как устроить хранение логов так, чтобы взлом самой машины не давал возможности стереть или подделать историю событий на ней.

Почему локальный лог бесполезен как улика

Логика простая и неприятная: root на сервере — это полный контроль над файловой системой, включая /var/log, бинарные журналы journald, файлы auditd и историю команд шелла. Атрибут неизменяемости файла (chattr +i или +a) — не защита от root на этой же машине: пока capability CAP_LINUX_IMMUTABLE доступна процессу с правами root, снять атрибут можно одной командой chattr -i. То же самое с правами на файл — chmod 000 не спасает root, который может поменять права обратно.

Отдельно стоит история с journald: бинарный формат журнала выглядит как «защита», но это не так — journalctl --vacuum-time=1s или прямое удаление файлов в /var/log/journal/ работают из-под root так же легко, как rm для текстового лога. Про то, как это устроено и где журнал теряет строки даже без злого умысла, у нас есть отдельный разбор.

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

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

Принцип: копия логов должна жить отдельно и появляться сразу

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

Схема на уровне архитектуры:

[app-сервер 1] --TLS--\
[app-сервер 2] --TLS----> [лог-коллектор] --> (архив / SIEM / Graylog / Loki)
[app-сервер 3] --TLS--/

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

Транспорт для передачи — rsyslog (стоит почти на всех дистрибутивах) или syslog-ng, реже — специализированные агенты (Filebeat, Vector, Fluent Bit), если логи уже собираются в Graylog/Loki. Дальше — конкретная настройка на rsyslog, потому что она есть «из коробки» и не требует ставить дополнительный демон.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Настройка отправки в реальном времени: rsyslog на удалённый коллектор

На коллекторе включаем приём по TCP с TLS. Ставим сертификаты (можно свой внутренний CA — публичный не нужен, соединение внутреннее) и добавляем конфиг:

# /etc/rsyslog.d/10-collector.conf на сервере-коллекторе
module(load="imtcp"
       StreamDriver.Name="ossl"
       StreamDriver.Mode="1"
       StreamDriver.AuthMode="x509/name"
       StreamDriver.PermittedPeers=["app01.internal","app02.internal","app03.internal"])

input(type="imtcp" port="6514")

# складываем по имени источника, отдельная папка на каждый сервер
template(name="perHostLogs" type="string"
         string="/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log")

*.* action(type="omfile" DynaFile="perHostLogs")

На каждом прод-сервере (источнике) — переслать всё исходящим потоком, сразу, без промежуточных файлов-накопителей:

# /etc/rsyslog.d/60-remote-forward.conf на app-сервере
module(load="omfwd")

action(
  type="omfwd"
  target="10.10.10.5" port="6514" protocol="tcp"
  StreamDriver="ossl" StreamDriverMode="1"
  StreamDriverAuthMode="x509/name"
  StreamDriverPermittedPeer="log-collector.internal"
  queue.type="LinkedList"
  queue.filename="fwdq01"
  queue.saveOnShutdown="on"
  queue.maxDiskSpace="1g"
  action.resumeRetryCount="-1"
  action.resumeInterval="5"
)

action.resumeRetryCount="-1" — значит «пытаться доставить бесконечно», а не «отвалиться и молчать», если коллектор временно недоступен. queue.type="LinkedList" держит недоставленные сообщения в памяти с возможностью сброса на диск при перегрузке очереди — это важно, потому что при обрыве связи или перезагрузке процесса rsyslog события не должны теряться безвозвратно, пока их не удалось отправить.

Отдельно нужно перенаправить journald и auditd, а не только текстовые файлы в /var/log:

  • journald — модуль imjournal в rsyslog читает бинарный журнал и передаёт его дальше по тому же каналу, либо используйте штатную пару systemd-journal-upload/systemd-journal-remote для аутентифицированной передачи журнала на отдельный сервер.
  • auditd — плагин audisp-syslog (или au-remote в новых версиях audit) пересылает события аудита через тот же syslog-канал, не дожидаясь, пока кто-то вручную заберёт /var/log/audit/audit.log.

Без переноса этих двух источников вы соберёте только «обычные» логи приложений, а самые интересные для расследования вещи — кто зашёл по SSH, кто выполнил sudo, какой процесс что открыл — останутся только локально.

Почему важна именно задержка между событием и отправкой

Централизованный сбор не спасает, если события копятся локально часами и улетают на коллектор пакетом раз в сутки по cron — за это время атакующий успеет получить root и почистить как раз тот файл, который ещё не отправлен. Смысл не в «логи где-то есть», а в том, что окно между записью события и его появлением на независимой машине должно быть минимальным.

Что на это влияет:

  • Способ доставки. Прямая потоковая передача через omfwd в rsyslog отправляет строки почти сразу по мере поступления, а не батчами. Это принципиально отличается от схемы «раз в час rsync лог-файлов на бэкап-сервер» — при такой схеме у атакующего есть весь час, чтобы почистить файл до следующей синхронизации.
  • Настройка очереди на отправку. Слишком большой queue.dequeueBatchSize или намеренная буферизация «для эффективности» откладывает фактическую отправку. Для логов безопасности лучше отправлять мелкими порциями чаще, а не экономить на количестве TCP-пакетов.
  • Частота flush для auditd. По умолчанию auditd может буферизовать запись на диск. В /etc/audit/auditd.conf стоит явно выставить:
  flush = INCREMENTAL_ASYNC
  freq = 1

Это заставляет auditd сбрасывать буфер на каждое событие (точнее — очень часто), а не копить пачками.

  • Как приложение пишет логи. Если сервис пишет в локальный файл, а его забирает агент со scan_frequency в 30–60 секунд (типичная настройка для Filebeat/Vector «по умолчанию») — это тоже задержка, в которую атакующий укладывается без проблем. Там, где возможно, лучше писать сразу в syslog (logger в shell-скриптах, встроенные syslog-хендлеры в приложении) вместо файла с последующим чтением.

Важная оговорка: на практике задержка доставки при нормальной сети и не перегруженном коллекторе обычно укладывается в доли секунды — но это ориентир, а не гарантированная цифра, и зависит от вашей сети, нагрузки на коллектор и настроек очереди. Гарантий «доставим за N миллисекунд» ни один из этих инструментов не даёт — задача просто сократить окно уязвимости с часов до долей секунды, а не свести его к нулю.

Append-only хранение: защита уже принятых записей на коллекторе

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

Практические уровни защиты на коллекторе:

1. chattr +a на активные файлы логов. Атрибут +a разрешает только дописывание в конец файла, запрещая изменение уже записанных байт и удаление файла:

touch /var/log/remote/app01/auth.log
chattr +a /var/log/remote/app01/auth.log
lsattr /var/log/remote/app01/auth.log
# ----a---------------- /var/log/remote/app01/auth.log

Это не защита от root на самом коллекторе (у root там та же capability, что снимает атрибут), но это защита от процесса-приёмника логов, если он скомпрометирован не полностью, и от случайной перезаписи скриптами.

2. После ротации — chattr +i (полная неизменяемость) на закрытые файлы. Пока файл активен, в него дописывают, поэтому нужен +a. Как только logrotate его закрыл — можно и нужно перевести в +i, потому что дописывать в архивный файл уже не должны:

# postrotate-хук в /etc/logrotate.d/remote-logs
postrotate
    for f in /var/log/remote/*/*.log.1; do
        chattr +i "$f" 2>/dev/null
    done
    for f in /var/log/remote/*/*.log; do
        chattr +a "$f" 2>/dev/null
    done
endscript

3. Второй независимый экземпляр в объектном хранилище с блокировкой записи. Если хочется защиты, которая переживёт даже полную компрометацию коллектора, архивные куски логов стоит периодически (например, раз в час) выгружать в S3-совместимое хранилище с Object Lock (MinIO, любой S3-совместимый провайдер с поддержкой WORM) в режиме governance или compliance — тогда объект нельзя удалить или перезаписать даже с валидными учётными данными до истечения срока блокировки. Это ровно та же логика, что применяется к неизменяемым бэкапам — подробно про механику chattr, ZFS read-only снапшотов и Object Lock применительно к бэкапам разобрано в статье неизменяемый бэкап: защита копий от того, кто уже внутри; для логов работают ровно те же инструменты, просто источник данных — не дамп базы, а поток syslog.

Комбинация «+a на активный файл + +i на закрытые + периодическая выгрузка в WORM-хранилище» даёт три независимых барьера, каждый из которых нужно обойти отдельно, а не один общий, снимаемый одной командой.

Защита самого коллектора: он тоже цель, причём привлекательная

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

  • Отдельная машина, без совмещения ролей. Коллектор не должен быть тем же сервером, где крутится сайт или база — иначе взлом веб-приложения даёт прямой путь к логам одним прыжком.
  • Изоляция по сети. Порт приёма логов (в примере — TCP/6514) открыт только для IP-адресов известных источников, в идеале — только внутри VPN/WireGuard-туннеля, без публикации в открытый интернет. SSH на коллектор — только с отдельного admin-сегмента, ключи и учётные записи не пересекаются с теми, что используются на прод-серверах.
  • Отдельные учётные данные. Если атакующий украл SSH-ключ с прод-сервера, этот же ключ не должен подходить к коллектору. Общий ключ или общий сервис-аккаунт на всей инфраструктуре сводит на нет весь смысл разделения.
  • Минимум установленного софта. Коллектору не нужен веб-сервер, не нужны лишние сервисы — чем меньше поверхность атаки, тем меньше шансов, что его скомпрометируют тем же способом, что и источник.
  • Независимый бэкап хранилища логов. Правило 3-2-1 применимо и здесь: копия архивных логов должна лежать не только на диске коллектора, но и в третьем месте, до которого скомпрометированный коллектор дотянуться не может (тот же принцип, что и для обычных бэкапов).
  • Периодический аудит самого коллектора. Раз он критичен для расследования любых будущих инцидентов, его собственная защищённость должна проверяться не реже, а по-хорошему чаще, чем у обычных серверов.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

У меня один VPS, второго сервера под коллектор нет — что делать?

Даже минимальный второй VPS (например, самый младший тариф) с единственной задачей «принимать логи по TLS и держать их append-only» закрывает основную дыру. Это не обязательно мощная машина — нагрузка на приём текстовых логов невелика, важнее сеть и диск под архив.

Разве auditd сам по себе не защищает логи, если настроить -e 2 (immutable mode)?

Режим -e 2 в auditd делает конфигурацию аудита неизменяемой до перезагрузки — это защищает правила аудита от отключения на лету, но не защищает уже записанный audit.log от root, который может напрямую удалить файл или обнулить диск. Это полезная мера, но она решает другую задачу и не заменяет вынос копии на отдельный сервер.

Можно обойтись UDP вместо TCP+TLS — это же проще?

UDP-syslog проще в настройке, но не гарантирует доставку (пакеты могут теряться без уведомления) и передаёт данные в открытом виде — их можно перехватить или подделать по пути. Для логов, которые могут стать доказательной базой при инциденте, разумнее TCP с очередью на диске и TLS с проверкой сертификата пира.

Что делать с логами внутри Docker-контейнеров?

У контейнеров свой logging driver — проще всего настроить syslog или fluentd в качестве драйвера логирования Docker (--log-driver=syslog или в daemon.json), чтобы вывод контейнера сразу уходил на тот же удалённый коллектор, а не оставался только в локальном json-log файле хоста.

Сколько дополнительно стоит такая схема?

В деньгах — стоимость ещё одного небольшого сервера под коллектор плюс место в объектном хранилище под архив (обычно недорого, логи хорошо сжимаются). В работе — час-два на первичную настройку rsyslog с TLS и логротейта с chattr, дальше схема работает без ручного вмешательства.

Нужно ли шифровать сами файлы логов на диске коллектора, а не только канал передачи?

Если в логах могут оказаться чувствительные данные (токены, часть запросов с персональными данными), шифрование архивного тома имеет смысл, но это отдельная задача про конфиденциальность, а не про защиту от переписывания — append-only и шифрование диска решают разные проблемы и обычно применяются вместе.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →