Запрос, на который вы ответили, — теперь запись, которую храните вы
Всё остальное в этой серии смотрит наружу: скрыть исходный сервер, зашифровать диск, выбрать юрисдикцию, не дать кому-то ещё вести на вас лог. А это руководство смотрит на саму машину: сервер, который отвечает на запросы, по умолчанию записывает, кто их сделал, — причём сразу в нескольких местах и со сроком хранения, который никто не выбирал.
Посмотрите, что скопилось на образе по умолчанию через две недели после развёртывания. В /var/log/nginx/access.log — по строке на каждый запрос: адрес, метка времени, путь, referrer, user agent; ротация ежедневно, хранение две недели. В /var/log/auth.log — каждая SSH-сессия, включая исходный адрес каждой вашей. В журнале — те же события ещё раз, плюс всё, что ваши сервисы напечатали в stderr. Если запущен Docker, у каждого контейнера свой JSON-лог, у которого из коробки вообще нет ограничения по размеру. Если запущен fail2ban, у него есть лог каждого адреса, который он когда-либо банил, и база SQLite с теми же данными, — и адреса в обоих местах полные.
Ничего из этого не злонамеренно, и большая часть по-настоящему полезна — примерно час после того, как что-то сломалось. Проблема — в форме: полная детализация, долгий срок хранения, случайно, а не по решению. Про каждый из этих файлов стоит спрашивать не "это чувствительные данные?", а "что я реально сделаю со строкой, которую записал три недели назад?" Для подавляющего большинства строк на подавляющем большинстве серверов ответ — ничего, а запись, которую вы никогда не прочтёте, — чистый риск: при компрометации, при бэкапе, который переживёт саму машину, при появлении того, кто однажды её запросит.
Формальное название этого решения — минимизация данных, и это самая бесспорная идея во всей защите данных: собирайте то, что нужно для дела, храните, пока это нужно, а потом прекращайте. Дальше это применяется к машине, которой вы реально управляете, — при условии, что вы уже прошли первый час после развёртывания и на машине действительно есть что защищать.
SP·02Шесть логов, и два из них называют людей
Прежде чем что-то менять, составьте опись. Небольшой VPS на Debian или Ubuntu, обслуживающий веб-сервис, обычно пишет шесть потоков данных, и они пересекаются сильнее, чем кажется, — одно и то же событие нередко попадает сразу в три файла с тремя разными сроками хранения.
Access-лог nginx — тот, что называет ваших посетителей поимённо. По строке на запрос: адрес клиента, точный путь, referrer и строка user agent, достаточно подробная, чтобы самой по себе служить слабым отпечатком. Error-лог nginx — тот, про который забывают: он записывает client: 203.0.113.9 при каждом таймауте апстрима, каждой 403-й и каждом некорректном запросе, — и, в отличие от access-лога, его формат фиксирован и не поддаётся шаблонизации.
Лог аутентификации — /var/log/auth.log в семействе Debian, /var/log/secure в семействе RHEL — называет поимённо вас. Каждая строка о принятом входе по publickey несёт ваш исходный адрес и отпечаток ключа, который открыл сессию. Тот, кто прочитает месяц этого лога, узнает, из каких сетей вы администрируете, в какие часы и сколько у вас разных ключей. На анонимно оформленной машине этот файл нередко раскрывает больше, чем всё, что породили ваши посетители.
Журнал хранит копию почти всего перечисленного выше, плюс stdout и stderr каждого юнита, и при установке по умолчанию ему разрешено расти до 10% файловой системы, но не больше 4 ГБ, — после чего он начинает вытеснять самые старые записи. На любом диске от 40 ГБ этот потолок — это полные 4 ГБ, а при объёмах, которые выдаёт небольшой сервер, это многие месяцы.
Логи приложения — джокер в этой колоде. Фреймворк в режиме отладки записывает полные URL вместе с query-строками, а query-строки то и дело несут в себе токены сессий, ссылки для сброса пароля и поисковые запросы. PHP-FPM можно настроить так, чтобы он вёл собственный access-лог, дублирующий строку запроса, которую nginx уже записал, — причём в файле, которого настройки nginx вообще не касаются.
Логи контейнеров — самые тихие. У драйвера json-file, который Docker использует по умолчанию, нет ротации, пока вы её не настроите, так что /var/lib/docker/containers/*/*-json.log хранит всё, что контейнер когда-либо сказал с момента создания. Это частый способ обнаружить, что политика хранения "14 дней" на самом деле держит одиннадцать месяцев, — и это тот же класс сюрпризов, что и правила firewall, которые Docker прописывает в обход ufw.
Из шести потоков два несут идентификаторы людей, которые стоит сознательно минимизировать: access-лог nginx (ваши посетители) и лог аутентификации (вы). Остальным по большей части нужны только потолок по размеру и более короткий срок хранения.
SP·03Усекайте при записи, а не при ротации
Первый инстинкт — логировать как обычно, а чистить потом: ночная cron-задача, которая переписывает вчерашний файл, вырезая из него адреса. Не стройте так. Задача-чистильщик означает, что необработанные адреса реально лежали на диске — до суток, — и за эти сутки они успели попасть в снапшот вашего бэкапа, возможно, в блочный снапшот хостинга, и осесть в том, что файловая система сделала со старыми блоками. Хуже того, такая задача — лишняя подвижная часть: она молча падает именно в ту неделю, когда диск заполнился, и ничто не подскажет вам, что вчерашний файл по-прежнему хранит адреса целиком.
Единственное сокращение данных, на которое реально можно положиться, — то, что происходит ещё до записи строки. В nginx это блок map, вычисляемый в момент логирования: он переписывает адрес в новую переменную, а log_format использует эту новую переменную вместо $remote_addr. Полный адрес никогда не сериализуется. После этого нечего чистить, нечего планировать по расписанию и нечего сломать в тот день, когда вы не следите.
# /etc/nginx/conf.d/00-privacy-log.conf -- http context, loaded before the sites
map $remote_addr $ip_trunc {
# IPv4: keep the /24, zero the host part
~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0";
# IPv6: keep the first two groups, drop the rest
~(?<v6>[^:]+:[^:]+): "${v6}::";
# anything the two patterns cannot parse -- including compressed forms
# like ::1 -- falls through here, i.e. fails closed rather than open
default "0.0.0.0";
}
log_format privacy '$ip_trunc - - [$time_local] "$request" $status '
'$body_bytes_sent "$http_referer" "$http_user_agent" '
'rid=$request_id rt=$request_time';
Три детали решают, заработает ли это на самом деле. Первое: блок map обязан жить в контексте http — внутри блока server nginx откажется стартовать. Проще всего гарантировать это, положив файл в conf.d/ с именем, которое сортируется одним из первых.
Второе: если вы работаете за Cloudflare или любым другим edge, проверьте, что на самом деле содержит $remote_addr. Модуль realip подменяет его на настоящий адрес клиента ещё в начале обработки запроса, а адрес, с которым установлено соединение, сохраняет в $realip_remote_addr. Этот порядок вам на руку: map выполняется в момент логирования, поэтому усекается адрес настоящего посетителя, а не edge-узла. Но это же означает, что если вы не настроили realip, то усекаете адрес Cloudflare и не узнаёте вообще ничего, пока настоящий адрес лежит в заголовке.
Третье: проверьте всю остальную строку формата. Усечение $remote_addr не даёт ровным счётом ничего, если строка всё равно заканчивается на "$http_x_forwarded_for" или несёт $http_cf_connecting_ip, — а очень многие стандартные форматы и форматы из панелей управления включают одно из двух. Полный адрес лежит в заголовках запроса, и он не попадёт в файл только в том случае, если вы уберёте из формата каждый заголовок, который его несёт.
Обратите внимание, что встало на его место: $request_id — случайная 32-символьная hex-строка, которую nginx генерирует на каждый запрос. Это и есть поворотная точка для следующего раздела.
Что ломает усечение, и три задачи, которые лог решает на самом деле
Всегда находится тот, кто возражает: анонимизированные логи бесполезны, — и он наполовину прав, потому что "лог" — это три не связанные друг с другом задачи под одним именем файла, и адрес нужен только для одной из них.
Задача первая: заблокировать того, кто долбит вас прямо сейчас. Для этого нужен полный адрес, и нужен он в течение секунд. Завтра он уже не нужен. Обычный инструмент для этого — fail2ban, и он в буквальном смысле не может работать с усечённым файлом: бан 203.0.113.0 банит один ни в чём не повинный хост, а атакующий остаётся на связи. Но для этой задачи достаточно файла, который живёт один день, — или вообще без файла: собственные limit_req и limit_conn у nginx хранят состояние в разделяемой памяти, реагируют за микросекунды вместо того, чтобы ждать опроса от fail2ban, и ничего не пишут на диск.
Задача вторая: разобраться, почему этот запрос вернул 500-ю. Здесь нужна корреляция, а не идентификация. ID запроса, протянутый из nginx в приложение, связывает строку access-лога, ошибку апстрима и трассировку стека приложения в одно событие, — а именно это вы и пытались получить, хватаясь за адрес. На практике ID даже лучше: он переживает клиента в мобильной сети, у которого адрес меняется посреди сессии, и не устаревает, когда четверо посетителей делят один адрес CGNAT.
Задача третья: понимать трафик в динамике. Объём, смесь кодов состояния, какие пути популярны, не сорвался ли краулер с цепи. Здесь усечённого адреса вполне достаточно, и уцелевшей после усечения /24 хватает, чтобы увидеть, что одна сеть даёт 40% ваших запросов.
Так что суть не в том, чтобы "логировать меньше", а в том, чтобы разделить по времени жизни: поток полной детализации живёт один день и питает инструменты блокировки, а усечённый поток живёт столько, сколько вам нужно для статистики. Оба пишет nginx в один и тот же момент, так что между ними нет промежуточной обработки и нет окна, в котором не то лежало бы на диске дольше, чем задумано.
# inside the server block, or in a snippet included by it
access_log /var/log/nginx/access.log privacy; # truncated, keep for weeks
access_log /var/log/nginx/security.log secip; # full address, keep for a day
# static assets are pure noise in a privacy log -- drop them entirely
location ~* \.(?:css|js|svg|png|jpe?g|webp|woff2?|ico)$ {
access_log off;
expires 30d;
}
Формат secip рядом с другим — однострочник: $remote_addr, метка времени, запрос и статус, больше ничего. Это единственный файл на машине, где допускается держать полный адрес посетителя, и поэтому его срок хранения — это одно решение в одном месте, а не свойство, которое приходится держать в уме сразу для шести файлов.
journald, auth.log и след, который ведёт обратно к вам
О приватности посетителей пишут охотно. А вот след администратора важен именно на машине, весь смысл которой в том, что ваше имя к ней не привязано, и почти весь этот след лежит всего в двух местах.
/var/log/auth.log пишет строку Accepted publickey на каждую открытую вами сессию — с вашим исходным адресом и отпечатком ключа. За месяц это готовое расписание того, когда вы работаете, и список сетей, которыми вы пользуетесь. Если вы всегда подключаетесь через один и тот же туннель, это один и тот же повторяющийся адрес, и в общем-то скучно. Если вы подключаетесь откуда придётся, это уже история поездок.
Журнал хранит те же события, плюс всё, что напечатали ваши юниты, и настройки по умолчанию у него щедрые: SystemMaxUse= — это 10% файловой системы, а MaxRetentionSec= не задан вовсе, то есть никакого ограничения по времени нет — только по размеру. На тихом сервере эта комбинация хранит данные месяцами.
Здесь есть настоящий компромисс, и его стоит проговорить прямо, а не отмахнуться. Логи, которые описывают вас, — это те же логи, которые расскажут, как кто-то проник внутрь. Задайте Storage=volatile — и журнал будет жить только в оперативной памяти, исчезая при перезагрузке: по-настоящему приватно и по-настоящему бесполезно тем утром, когда вы обнаружите процесс, который не запускали сами, — потому что первая же перезагрузка, устроенная атакующим, стёрла улики. Для большинства разумная середина — это постоянное хранение с жёстким потолком и коротким сроком: достаточно долгим, чтобы расследовать инцидент, замеченный в течение недели, и достаточно коротким, чтобы файл не превращался в дневник.
Две практические тонкости, на которых обычно спотыкаются. Образы Debian и Ubuntu расходятся в том, установлен ли rsyslog; если на вашей машине есть /var/log/auth.log, значит его пишет rsyslog, и лимиты journald на этот файл вообще не действуют — это работа logrotate. А ForwardToSyslog= — это то, что передаёт данные из журнала в rsyslog, так что отключение этой опции на машине, где стоят оба, избавляет вас от хранения двух копий одного и того же с двумя разными сроками хранения.
Ваш срок хранения — это тот срок, который назначили ваши бэкапы
Вот что сводит на нет всю аккуратную работу, описанную выше, — и это незаметно, пока вы сами не пойдёте искать.
Допустим, logrotate хранит логи nginx четырнадцать дней, и вас это устраивает. Теперь добавьте бэкап вне сервера, который вы совершенно правильно завели: ежедневный прогон Borg или restic с хранением семи дневных, четырёх недельных и шести месячных архивов. Каждый из этих архивов содержит /var/log таким, каким он был в день прогона. Самому старому месячному архиву — полгода, и в нём лежат те четырнадцать дней логов, что были актуальны тогда. Ваш реальный срок хранения логов — не четырнадцать дней. Это полгода, в зашифрованном репозитории, который нельзя прогрепать без восстановления, на второй машине, в другой стране.
Есть ровно два честных решения. Исключить каталоги логов из бэкапа — это едва ли не единственное на сервере, что почти всегда можно пересобрать заново или обойтись без этого, и восстановление без /var/log не становится от этого хуже. Либо включить их осознанно и признать, что ваш реальный срок хранения — это срок хранения репозитория, и в таком случае сказать об этом прямо в публикуемой политике, потому что альтернатива — заявленная политика, которой противоречит ваша же инфраструктура.
# exclude logs from the backup, and prove it took
borg create --stats \
--exclude '/var/log' \
--exclude '/var/lib/docker/containers' \
::'{hostname}-{now:%Y-%m-%d}' /
# the check that matters: can you still find an address in the newest archive?
borg list ::"$(borg list --short --last 1)" | grep -c '^var/log/' || echo "clean"
Раз уж вы в этом настрое — два соседних варианта той же проблемы. Снапшоты вашего хостинга — не ваши. Снапшот на уровне гипервизора фиксирует диск таким, каким он был, включая логи, которые вы с тех пор уже сротировали прочь, и живёт он на хранилище провайдера по правилам хранения провайдера, — и это ещё один довод в пользу того, что адреса вообще не стоило записывать полностью, а не повод сделать что-то хитрое задним числом.
И не хватайтесь за shred на VPS. Перезапись файла на виртуальном диске с динамическим выделением места, поверх файловой системы с copy-on-write, поверх SSD, который переназначает блоки ради выравнивания износа, не гарантирует перезапись физических ячеек, где эти данные лежали. Безопасное удаление на арендованном, виртуализированном хранилище — это театр. Работает только то сокращение данных, которое вы сделали в момент записи; всё, что после, — это попытка, которую нельзя проверить. Шифрование тома меняет это уравнение, — но меняет его до того, как данные записаны, и это тот же урок ещё раз.
Копии, которые вам не подконтрольны
Минимизация на вашей собственной машине — это только один слой из нескольких, и ясное понимание остальных как раз и не даёт этому превратиться в ложное чувство полноты защиты.
Ваша хостинг-сеть видит запись о потоке. Источник, назначение, порты, объём, тайминги — по каждому соединению внутрь машины и наружу, независимо от того, ведёте вы хоть какой-то лог или нет. Никакая настройка на сервере этого не меняет. Во многом именно поэтому юрисдикция, в которой стоит машина, — реальная переменная, а не маркетинговый ход: что именно закон требует от сети — очень сильно зависит от страны.
Ваш CDN или edge логирует на своей стороне, ещё на периметре. Если Cloudflare терминирует TLS за вас, у него есть строка запроса и адрес клиента ещё до того, как в дело вступает ваш сервер, — по собственному графику хранения и в ответ на собственные запросы от правоохранителей. Усечение вашего origin-лога не дотягивается назад сквозь это. Запустить собственный edge — это версия того же самого, которую вы реально можете настроить, — и правила логирования из этого руководства в первую очередь относятся именно к edge-машине, потому что именно туда приходят адреса без усечения.
Трекеры ошибок и аналитика сами увозят это с машины за вас. Sentry и большинство похожих на него сервисов по умолчанию прикрепляют IP клиента к каждому событию; настройка обычно называется примерно как send_default_pii, и её стоит проверить, а не считать чем-то само собой разумеющимся. Любая облачная аналитика по своей конструкции — это сторонняя копия того самого access-лога, который вы только что потратили целый вечер на усечение.
Почта протекает сильнее всего остального. Если хоть что-то на машине отправляет письма, заголовки несут хост и адрес отправителя, а каждый узел-ретранслятор на этом пути хранит копию конверта с отметками времени. Форма обратной связи, которая шлёт вам письмо, — это лог, которым управляете не вы.
Всё это не делает работу с локальными логами бессмысленной — именно локальная копия попадёт в чужие руки вместе с машиной при изъятии, утечёт при взломе или будет передана вами самими. Это просто тот единственный слой, который вы контролируете полностью, — и ошибка в том, чтобы принимать его за всю картину целиком.
SP·08Минимизация по проекту, а не удаление по требованию
Здесь стоит быть точным, потому что эти два понятия постоянно путают, а разница между ними — это разница между стандартной инженерной практикой и тем, чего делать не следует.
Заранее решить, что именно ваш сервис собирает и как долго это хранит, — обычная, задокументированная и всячески поощряемая практика. В GDPR это два из базовых принципов — минимизация данных и ограничение срока хранения, — и более короткий срок хранения логов — это как раз то, чего аудиторы просят, а не то, против чего они возражают. У оператора сайта или клиента хостинга в ЕС нет общей обязанности хранить логи трафика; директива о повальном хранении данных, которая раньше как будто это подразумевала, была признана недействительной Судом Европейского союза в 2014 году, а уцелевшие национальные законы в основном касаются операторов электросвязи, а не тех, кто просто держит веб-сервер. В США тоже нет общего требования о хранении данных для операторов сайтов.
Уничтожение конкретных записей уже после того, как вас о них официально уведомили, — совершенно другое действие. Официальный запрос на сохранение данных, требование сохранить доказательства для суда, решение суда или запрос полиции меняют то, что вам можно делать с данными, существующими на этот момент, и "это удалила моя политика хранения" не работает как оправдание, если вы ускорили удаление именно из-за уведомления. Ничего из этого руководства к такой ситуации не относится. Политику хранения устанавливают в спокойный вторник и потом оставляют в покое; если её укорачивают только тогда, когда что-то уже случилось, это была не политика.
Та же граница проходит через весь остальной сайт: есть настоящая черта между инженерией приватности и позицией "bulletproof", которая продаёт себя как неуязвимость. Сервис, спроектированный так, что он никогда не накапливает историю посетителей, уверенно стоит по правильную сторону этой черты — в одном ряду с шифрованием дисков и отказом спрашивать у пользователей email, который вам не нужен.
Отсюда два практических следствия. Если вы публикуете политику конфиденциальности, приведите указанные в ней сроки хранения в соответствие с тем, что реально лежит на диске: заявленные четырнадцать дней при реальных шести месяцах в репозитории бэкапов — это тот разрыв, что на бумаге превращает добросовестного оператора в недобросовестного. И запишите это решение для себя — хотя бы комментарием в начале файла logrotate, — потому что объяснять эти цифры через полтора года придётся именно вам, а вы не вспомните, почему стояла семёрка. Ничто из вышесказанного не является юридической консультацией; если вы работаете там, где действуют отраслевые требования к срокам хранения, сверьтесь с ними применительно к своей ситуации, прежде чем что-либо сокращать.
SP·09Шаг за шагом
-
01
Узнайте, что машина уже успела накопить
Не настраивайте ничего, пока не измерили текущее состояние. Смысл этого прохода — найти файл, о котором вы забыли, а на большинстве машин это либо лог контейнера, либо лог приложения, в который никто не заглядывал со времён деплоя.
# biggest log files anywhere on the box, largest last sudo du -ah /var/log /var/lib/docker/containers 2>/dev/null \ | sort -h | tail -20 # how much disk the journal holds, and how far back it goes journalctl --disk-usage journalctl --output=short-iso | head -1 # how old is the oldest nginx line still on disk? zcat -f /var/log/nginx/access.log* 2>/dev/null | head -1
Затем посмотрите на одну строку из каждого файла и спросите себя, что именно она идентифицирует. Команда ниже считает, сколько разных полных адресов сейчас можно достать из ваших веб-логов, — обычно именно это число и служит лучшим доводом в пользу всего остального в этом руководстве.
zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | wc -lЗапишите, что у вас получилось. В конце вы прогоните те же самые команды, чтобы доказать, что изменения подействовали.
-
02
Усекайте адрес клиента ещё до того, как nginx запишет строку
Создайте map и оба формата в файле, который подключается в контекст
http. В Debian и Ubuntu/etc/nginx/conf.d/подключается изnginx.confраньше конфигов сайтов, — то есть это как раз то самое место.sudo tee /etc/nginx/conf.d/00-privacy-log.conf > /dev/null <<'EOF' # The client address is reduced here, at log time, and never written in full # to the long-retention file. $realip_remote_addr still holds the connecting # address if you need it while debugging a proxy problem. map $remote_addr $ip_trunc { ~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0"; ~(?<v6>[^:]+:[^:]+): "${v6}::"; # anything the two patterns cannot parse -- including compressed forms # like ::1 -- falls through here, i.e. fails closed rather than open default "0.0.0.0"; } # Long retention: no full address, no forwarded-for header, request id instead. log_format privacy '$ip_trunc - - [$time_local] "$request" $status ' '$body_bytes_sent "$http_referer" "$http_user_agent" ' 'rid=$request_id rt=$request_time'; # Short retention: the one file allowed to hold a complete address. log_format secip '$remote_addr [$time_local] "$request" $status'; EOF sudo nginx -t && sudo systemctl reload nginxТеперь направьте сайт на них. В своём блоке server замените существующую строку
access_logэтой парой и заодно отключите логирование для статических файлов.access_log /var/log/nginx/access.log privacy; access_log /var/log/nginx/security.log secip;
Перезагрузите nginx, откройте страницу и посмотрите на результат. Первое поле должно заканчиваться на
.0, а в строке должно быть значениеrid=.sudo nginx -t && sudo systemctl reload nginx curl -s -o /dev/null https://your-domain.example/ sudo tail -1 /var/log/nginx/access.log
Если адрес всё ещё полный, значит где-то ниже блок server перекрывает формат заново, —
grep -rn access_log /etc/nginx/найдёт ту строку, которая побеждает. -
03
Держите полные адреса только там, где на них что-то реагирует
Если у вас запущен fail2ban, сейчас он читает тот самый файл, который вы только что усекли. Перенаправьте его на лог безопасности и дайте этому логу жить один день, чтобы полные адреса истекали сами собой.
sudo tee /etc/fail2ban/jail.d/nginx-privacy.local > /dev/null <<'EOF' [nginx-http-auth] enabled = true logpath = /var/log/nginx/error.log [nginx-botsearch] enabled = true logpath = /var/log/nginx/security.log maxretry = 6 findtime = 10m bantime = 1h EOF sudo fail2ban-client reload
Затем разберитесь с собственной памятью fail2ban, до которой большинство вообще не доходит руками: он ведёт свой лог банов с настройкой по умолчанию
rotate 4 weekly, а также базу SQLite со всеми выданными банами.dbpurgeage— это то, что удаляет устаревшие строки из этой базы; выставьте значение, близкое к вашему самому долгому бану, а не оставляйте значение по умолчанию.sudo tee /etc/fail2ban/fail2ban.d/retention.local > /dev/null <<'EOF' [Definition] dbpurgeage = 2d loglevel = NOTICE EOF sudo systemctl restart fail2ban
Ещё лучше — для простых флудов делать эту работу прямо в nginx, где вообще ничего не записывается.
limit_reqдержит свои счётчики в разделяемой памяти, реагирует за микросекунды вместо ожидания опроса и не оставляет никакой записи о том, кого он придержал, — за расчётом размера зон под реальной нагрузкой обращайтесь к runbook на первый час DDoS-атаки.# http context: keyed on the truncated address, so nothing complete is # held in memory either. 10m of shared state is plenty for a small site. limit_req_zone $ip_trunc zone=perip:10m rate=20r/s; # in the location you want protected limit_req zone=perip burst=40 nodelay;
Ключевание зоны по
$ip_truncвместо$binary_remote_addr— это осознанный компромисс: теперь лимит действует сразу на весь/24, так что загруженный офис за одним блоком делит один общий бюджет. Для небольшого сайта это обычно нормально, а иногда даже к лучшему; если нет — ключуйте зону по полному адресу: состояние ограничителя запросов живёт в разделяемой памяти и никогда не пишется на диск, так что оно вообще не входит в то, что вы здесь минимизируете. -
04
Ограничьте журнал и избавьтесь от второй копии
journald поддерживает drop-in-файлы, которые переживают обновления пакета — в отличие от прямого редактирования
journald.conf. Значения ниже хранят данные около недели — этого достаточно, чтобы расследовать в понедельник то, что началось в пятницу, — внутри жёсткого потолка в 200 МБ.sudo mkdir -p /etc/systemd/journald.conf.d sudo tee /etc/systemd/journald.conf.d/retention.conf > /dev/null <<'EOF' [Journal] Storage=persistent SystemMaxUse=200M SystemMaxFileSize=20M MaxRetentionSec=7day MaxFileSec=1day ForwardToSyslog=no EOF sudo systemctl restart systemd-journald journalctl --disk-usage
Перезапуск сразу применяет ограничения по размеру; срок хранения начинает действовать по мере ротации новых файлов, так что уже разросшийся журнал сожмётся при следующей ротации, а не мгновенно.
journalctl --vacuum-time=7dзаставит это произойти прямо сейчас, если вам нужно освободить диск сегодня же.ForwardToSyslog=noимеет значение на любом образе, где есть rsyslog: без этой настройки каждая запись журнала дополнительно дублируется в/var/log/syslog— уже по расписанию logrotate, а не journald, — и у вас оказывается две копии с двумя разными сроками жизни. Проверьте, какая ситуация у вас, прежде чем строить предположения:systemctl is-active rsyslog 2>/dev/null || echo "rsyslog not running" ls -la /var/log/auth.log /var/log/syslog 2>/dev/null
Если эти файлы существуют, ими управляет rsyslog, и их срок хранения задаётся на шаге шестом. Если их нет, журнал — единственная копия, и вы её только что ограничили.
-
05
Не дайте приложению заново залогировать то, что вы убрали
Всё, что было выше, не касается вашего приложения, а приложение в неправильном режиме с радостью запишет полный адрес, полный URL и токен сессии в собственный файл. Есть три вещи, которые стоит проверить.
У PHP-FPM в конфиге пула есть директива
access.log, по умолчанию закомментированная, но включённая в очень многих панелях управления. Если она включена, это вторая копия каждой строки запроса, — из файла, которого вся ваша работа с nginx никогда не касалась.grep -rn '^access.log' /etc/php/*/fpm/pool.d/ || echo "fpm access log off"
Уровень логирования вашего фреймворка решает, попадут ли на диск query-строки и тела запросов. Режим отладки в большинстве фреймворков логирует полный URL, а ссылка для сброса пароля — это и есть полный URL. Установите продакшен-уровень и убедитесь, что загружен именно он, а не тот, что лежит в файле, который вы считаете актуальным.
Драйвер Docker по умолчанию никогда не ротирует логи. Исправьте это на уровне демона, чтобы ограничение унаследовал каждый будущий контейнер. Учтите: это касается только контейнеров, созданных после перезапуска, — существующие держат свой текущий, ничем не ограниченный файл, пока их не пересоздадут.
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF' { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } EOF sudo systemctl restart docker docker inspect --format '{{.HostConfig.LogConfig}}' $(docker ps -q) 2>/dev/nullЕсли у контейнера уже накопился большой файл, реально обрезает историю только его пересоздание командой
docker compose up -d --force-recreate, — обычный перезапуск оставляет тот же самый лог-файл. -
06
Назначайте срок хранения осознанно — по одному месту на файл
logrotate — это то место, где живут все сроки для всего, что пишут rsyslog и nginx. Редактируйте готовую секцию вместо того, чтобы добавлять вторую: две секции с одним и тем же путём заставляют logrotate упасть с ошибкой дублирующейся записи и вообще перестать ротировать этот файл, а это самый частый способ, которым изменение срока хранения тихо превращается в его отсутствие.
# check for duplicates BEFORE editing, then again after sudo logrotate --debug /etc/logrotate.conf 2>&1 | grep -i 'duplicate\|error'
Для nginx двум файлам нужны разные сроки: усечённый может жить неделями, а тот, что хранит полные адреса, не должен пережить день. Добавьте отдельную секцию для лога безопасности — с другим путём, так что дублирования не будет, — и сократите срок у готовой секции.
sudo tee /etc/logrotate.d/nginx-security > /dev/null <<'EOF' # The only file on this box that holds complete client addresses. # One day, uncompressed so fail2ban can read it. Do not lengthen without # a reason you would be happy to write down here. /var/log/nginx/security.log { daily rotate 1 maxage 1 missingok notifempty nocompress create 0640 www-data adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript } EOF sudo sed -i 's/^\trotate 14$/\trotate 7/' /etc/logrotate.d/nginx sudo logrotate --debug /etc/logrotate.d/nginx-securityПроделайте ту же арифметику для
/etc/logrotate.d/rsyslog, если rsyslog установлен, — по умолчанию он хранит четыре неделиauth.log, а это четыре недели ваших собственных SSH-сессий. И запустите debug-проход ещё раз, напоследок: он печатает, какие именно файлы будут ротированы и удалены, и это единственный способ убедиться, что только что введённые вами цифры — это и есть действующие цифры. -
07
Докажите это, включая проверку через бэкап
Прогоните заново измерения из первого шага. Количество разных полных адресов в логе с долгим сроком хранения теперь должно перестать расти, а после одного цикла ротации — стать нулевым.
# should print 0 once the pre-change files have rotated out zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' \ | grep -v '\.0$' | sort -u | wc -l # and the journal should now be bounded journalctl --disk-usageЗатем — проверка, которую почти никто не делает: загляните внутрь бэкапа. Репозиторий, который всё ещё несёт в себе
/var/log, хранит ту самую детализацию и тот самый срок, на устранение которых вы только что потратили вечер, — и будет хранить их, пока жив ваш самый старый архив.borg list | head -3 # borg lists oldest first borg list ::"$(borg list --short | head -1)" 2>/dev/null \ | grep -c '^var/log/' || echo "no logs in oldest archive"
Если счётчик не нулевой, либо добавьте исключение, описанное раньше, и дайте старым архивам устареть самим, либо вычистите их вручную. Пока не произойдёт ни то, ни другое, ваш реальный срок хранения — это срок хранения репозитория, — и это, пожалуй, самая полезная фраза во всём этом руководстве, и её же проще всего забыть.
И напоследок — запишите цифры там, где их найдёт следующий человек: выбранный срок хранения, причину и дату. Комментария в начале файла logrotate достаточно. Всё, что настроено выше, — это решение, а решение, которое никто не сможет восстановить через год, снова становится дефолтом.


