Логи — фундамент эксплуатации: без них мы не поймём, почему деградирует SLA, где узкое место, как выглядит атака и какой именно запрос приводит к таймауту. Но логи одновременно и зона повышенных рисков: в них попадают IP-адреса посетителей, полные URI с параметрами (где часто оказываются e-mail, телефоны, токены), реферер с внешних ресурсов и даже уникальные идентификаторы устройств. Если бездумно хранить такие данные годами, рано или поздно придётся отвечать на вопросы информационной безопасности и соответствия требованиям приватности (включая GDPR), а в случае инцидентов — ещё и быстро очищать архивы.
В этой статье соберём практические приёмы для Nginx: анонимизация IP (обрезка для IPv4/IPv6, псевдонимизация), маскировка URI/реферера/агента и управляемый ретеншн логов. Приоритет — баланс: минимизируем персональные данные, но сохраняем достаточную наблюдаемость и пригодность логов для дебага и расследований.
Что именно опасно в веб-логах
Классический access_log содержит как минимум IP-адрес, метку времени, метод и путь, статус, байты ответа, реферер, user-agent. Уже этого достаточно, чтобы идентифицировать пользователя при корреляции с другими источниками. Проблема усиливается, если:
- В
$request_uriили$argsпередаётся PII: e-mail, телефон, номер договора, паспортные поля, координаты. - В реферере протекают токены или приватные параметры из внешних систем.
- IP логируются полностью и долго хранятся без целей и ограничений.
- Логируются X-Forwarded-For цепочки с реальными IP, хотя для задач эксплуатации достаточно укороченного префикса или псевдонима.
Базовый принцип приватности: логируйте минимально необходимое для вашей цели и не храните дольше, чем нужно.
Подходы: минимизация, анонимизация, псевдонимизация
Минимизация — не писать лишнего. Для веб-сервера это означает: вместо полного $request_uri писать $uri (без query string), не логировать реферер целиком, агрессивно сокращать user-agent и IP.
Анонимизация — сделать исходное значение невосстановимым. Для IP это обрезка младших битов: для IPv4 до /24, для IPv6 до /64 (или грубее). Для URI — нормализация и заменяющие маркеры, например /user/12345 на /user/:id.
Псевдонимизация — заменить значение стабильным псевдонимом (например, хэш с солью). Это даёт возможность корреляции событий без раскрытия исходных данных. В веб-логах это может быть «солёный» хэш IP и доменный реферер вместо полного URL.

Nginx: как устроен лог и где резать
Nginx даёт гибкий контроль за форматом через log_format и возможность предобработки значений через map. План действий обычно такой:
- Правильно определить реальный IP, если сервер за балансировщиком/CDN (
real_ip_headerиset_real_ip_from), а уже потом анонимизировать. - Определиться с стратегией IP: полностью убирать, обрезать, или выдавать псевдоним (хэш).
- Сформировать «безопасный» URI: без query string или с whitelisting параметров, маскировать переменные части в path.
- Огрубить реферер (оставить только домен) и user-agent (например, семейство и версию движка, без лишних уникальных признаков).
- Настроить ретеншн: ротация, компрессия, срок хранения, контроль доступа.
Если требуется полный контроль над конфигурацией и njs, удобнее работать на собственном VDS; на shared-площадках доступ к модулям и системным политикам может быть ограничен.
Минимальный формат без IP и без query string
Если вам достаточно корреляции по $request_id и агрегатам, самый строгий формат — вообще не логировать IP и параметры запроса. Это резко уменьшает риски, но оставляет пригодность для дебага по конкретному запросу via $request_id.
log_format main_priv '$time_local $request_id "$request" $status $body_bytes_sent "$server_name" "$http_user_agent"';
access_log /var/log/nginx/access.log main_priv;
В этом формате вместо $request_uri лучше логировать $request (метод и путь) или $uri, если нужно исключить query string.
Обрезка IP (IPv4/IPv6)
Чаще нужна середина: оставить сетевой контекст, но убрать конкретный хост. Для IPv4 обычно хватает обрезки до /24, для IPv6 — до /64. Для IPv4 это делается через map. IPv6 корректно обрезать регулярками сложнее; надёжнее использовать njs (если уже используете) или изначально не логировать IPv6 вовсе, если такова политика. Ниже — гибридный подход: обрезка IPv4 через map, IPv6 — через njs-функцию.
# 1) IPv4: заменяем последний октет на 0
map $remote_addr $ip4_trunc {
~^(?<a>\d+)\.(?<b>\d+)\.(?<c>\d+)\.\d+$ $a.$b.$c.0;
default "";
}
# 2) IPv6: через njs (пример), если модуль подключён
# http { js_import iputils from /etc/nginx/iputils.js; }
# js_set $ip6_trunc iputils.ipv6_trunc64;
# 3) Сводная переменная для IP любой семьи
map $remote_addr $ip_anon {
~: $ip6_trunc; # если двоеточие в адресе — это IPv6
default $ip4_trunc;
}
# Формат лога с анонимизированным IP и без query string
log_format main_priv '$time_local $request_id $ip_anon "$request" $status $body_bytes_sent "$host"';
access_log /var/log/nginx/access.log main_priv;
Пример njs-функции ipv6_trunc64 с грубой нормализацией и обрезкой до /64. В реальном проекте лучше реализовать полноценную нормализацию :: и нулей.
// /etc/nginx/iputils.js
function expand(groups) {
// упрощённое расширение, не для всех крайних случаев
let parts = groups.split('::');
let left = parts[0].length ? parts[0].split(':') : [];
let right = parts.length > 1 && parts[1].length ? parts[1].split(':') : [];
while (left.length + right.length < 8) right.unshift('0');
return left.concat(right).map(x => x || '0');
}
function ipv6_trunc64(r) {
let addr = r.variables.remote_addr;
if (addr.indexOf(':') === -1) return '';
let parts = expand(addr).slice(0, 4); // первые 64 бита
return parts.join(':') + '::';
}
export default { ipv6_trunc64 };
Если только начинаете внедрять IPv6 и окружение смешанное, посмотрите разбор по переходу и совместимости — материал про NAT64/DNS64 и IPv6 на VDS: NAT64/DNS64 и IPv6 на VDS.
Псевдонимизация IP: солёный хэш
Когда нужна корреляция по пользователю в пределах сессий/суток, но IP раскрывать нельзя, используйте псевдонимизацию: вычисляйте хэш IP с секретной солью. В Nginx это удобно делать через njs. Важно: соль храните в файловой системе с ограниченным доступом и ротируйте по расписанию (например, раз в 30 дней).
// /etc/nginx/iputils.js (добавим функцию хэша)
function sha1hex(input) {
var hash = require('crypto').createHash('sha1');
hash.update(input);
return hash.digest('hex');
}
function ip_salt_hash(r) {
var ip = r.variables.remote_addr || '';
var salt = r.variables.ip_salt || '';
if (!ip) return '';
return sha1hex(salt + '|' + ip).substr(0, 16); // короткий псевдоним
}
export default { ip_salt_hash };
# nginx.conf
# js_import iputils from /etc/nginx/iputils.js;
# env IP_SALT; # передайте соль через environment
js_set $ip_alias iputils.ip_salt_hash;
map $env_IP_SALT $ip_salt { default $env_IP_SALT; }
log_format main_priv '$time_local $request_id $ip_alias "$request" $status $body_bytes_sent';
access_log /var/log/nginx/access.log main_priv;
Такой псевдоним позволяет считать уникальных посетителей и расследовать события без доступа к исходным IP. При ротации соли кросс-периодная корреляция перестанет работать, что полезно для приватности.
Маскировка URI, реферера и user-agent
Даже при аккуратной работе с IP, в $request_uri и $http_referer может протекать PII. Базовая стратегия — логировать путь без параметров ($uri) и сильно огрублять реферер. Если есть обязательные параметры для аналитики, применяйте whitelist.
Отключаем query string или применяем whitelist
Самый простой путь — логировать только $uri. Если необходимо сохранить 1–2 параметра, лучше собрать «безопасную строку» вручную с использованием $arg_*.
# Жёстко: вообще без args
log_format main_noargs '$time_local $request_id "$uri" $status $body_bytes_sent';
# Мягко: whitelist параметров q и page
map "$arg_q|$arg_page" $args_safe {
default "";
~^\|$ "";
~^([^|]*)\|([^|]*)$ q=$1&page=$2;
}
log_format main_args '$time_local $request_id "$uri?$args_safe" $status $body_bytes_sent';
Обратите внимание: никакого свободного $args — только whitelisting. Если параметр отсутствует, строка будет чистой.
Маскируем переменные части path
Часто в path встречаются идентификаторы: числовые ID, UUID, e-mail-адреса. Их имеет смысл заменять на маркеры. Это удобно делать через map с регулярками.
map $uri $uri_masked {
~^/user/\d+$ /user/:id;
~^/order/\d+$ /order/:id;
~^/reset/(?<uuid>[0-9a-fA-F\-]{36})$ /reset/:uuid;
default $uri;
}
log_format main_mask '$time_local $request_id "$uri_masked" $status $body_bytes_sent';
Если в проекте используется человекочитаемый ID (например, e-mail в path), лучше на уровне приложения заменить схему адресации. До тех пор — маскируйте в логах.
Огрубляем реферер и user-agent
В реферере часто встречаются параметры сторонних систем. Рекомендуется оставлять только домен источника. User-Agent логируйте укороченным: достаточно движка и основной версии, либо вовсе замените на фиктивный маркер при чувствительных ресурсах.
# Домен из реферера
map $http_referer $ref_domain {
~^https?://(?<d>[^/]+)/ $d;
default "-";
}
# Урезанный user-agent (пример для иллюстрации)
map $http_user_agent $ua_short {
~Chrome/(?<v>\d+) Chrome/$v;
~Firefox/(?<v>\d+) Firefox/$v;
~Safari/(?<v>\d+) Safari/$v;
default "-";
}
log_format main_ref '$time_local $request_id $ip_anon "$uri_masked" $status "$ref_domain" "$ua_short"';
Итог: сильно меньше PII, сохраняется полезный контекст источника и класса клиента.
Ретеншн: сколько хранить и как удалять
Приватность логов невозможна без понятной политики хранения: срок, объём, ответственность за удаление. Технически это реализуется через ротацию, компрессию, удаление старых файлов и контроль прав доступа. На Linux для файловых логов Nginx обычно используют logrotate.
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
rotate 14 # 14 архивов, ~2 недели
missingok
notifempty
compress
delaycompress
dateext
create 0640 www-data adm
sharedscripts
postrotate
test -f /run/nginx.pid && kill -USR1 $(cat /run/nginx.pid)
endscript
}
Советы по ретеншну:
- Храните «подробные» логи минимально (например, 3–7 дней), агрегаты и метрики — дольше.
- Компрессия обязательна, но не заменяет удаление. Если нужны расследования за большие периоды — храните агрегированные отчёты.
- Разделяйте логи по классам чувствительности: публичные статики, API, авторизационные маршруты — отдельно и с разными сроками.
- Ограничьте доступ правами POSIX: владельцы — сервисные пользователи, группы — минимум необходимых.
journald и системные лимиты
Если вы используете journald (например, контейнеризованная среда или системная консолидация), установите лимиты по объёму и времени хранения.
# /etc/systemd/journald.conf (фрагмент)
SystemMaxUse=200M
SystemMaxFileSize=50M
MaxRetentionSec=14day
Storage=auto
После изменений перезапустите journald и убедитесь, что Nginx пишет куда нужно. Не забывайте, что журналы ядра и системные сервисы могут содержать IP и пользовательские данные, их ретеншн следует согласовать с политикой.

За балансировщиком и CDN: X-Forwarded-For без сюрпризов
Если веб стоит за балансировщиком или CDN, реальный IP приходит в заголовке цепочкой. Используйте real_ip_header и список доверенных set_real_ip_from, затем работайте уже с $remote_addr. Не пишите в логи всю цепочку $proxy_add_x_forwarded_for — это не нужно для эксплуатации и плохо для приватности.
Сначала корректно определите реальный IP, затем анонимизируйте. Не наоборот.
Метрики вместо сырого PII
Часть задач наблюдаемости лучше решать не логами, а метриками и агрегатами: гистограммы латентности, счётчики по маршрутам и статусам, выборки топ-URI без query string. Это уменьшает объём PII и ускоряет аналитические запросы. Если нужны выборки по источникам, храните домены, а не полные рефереры.
Юридические и организационные аспекты
Технические меры должны сопровождаться процессами: документируйте цель логирования, срок хранения, перечень полей и ответственных. Для пользовательских запросов на удаление данных обеспечьте оперативную очистку из архивов. Внутренний аудит и периодические ревизии шаблонов логов помогают удерживать баланс между эксплуатацией и приватностью.
Чек-лист внедрения
- Согласуйте цель логирования и минимальный набор полей для вашей команды.
- Определите стратегию IP: удалить, обрезать или псевдонимизировать.
- Перейдите на
$uriвместо$request_uri, настройте whitelist параметров. - Маскируйте переменные части path; реферер — до домена; user-agent — до семейства.
- Включите
$request_idдля корреляции событий без IP. - Настройте logrotate/journald: период, объём, компрессию и удаление.
- Ограничьте доступ к логам и зафиксируйте сроки хранения в регламенте.
- Проведите тесты: синтетические запросы с PII в path/args/referer не должны попасть в логи в явном виде.
Практические советы эксплуатации
Сначала разверните новые форматы в «теневом» логе: параллельно пишите в безопасный и старый формат, сравните пригодность для расследований. Через 1–2 недели удалите старый формат. Для критичных маршрутов (аутентификация, платёжные коллбэки) предусмотрите повышенный уровень логирования, но с теми же правилами анонимизации. Регулярно пересматривайте списки регулярных выражений для маскировки, чтобы не упустить новые шаблоны URI.
Если у вас несколько окружений, начинайте с теста и стейджа, затем выкатывайте на прод поэтапно, контролируя объём логов и успех парсинга в аналитическом стеке. Если управляете серверами через панели, пригодится сравнение популярных панелей: сравнение панелей VDS 2025. Не забывайте мониторить ошибки парсера: смена формата без обновления пайплайна часто ломает построение дашбордов.
Итог
Приватность логов — это не отказ от наблюдаемости, а грамотная настройка: минимизация и маскировка данных на входе, жёсткий ретеншн и контроль доступа, плюс осознанная псевдонимизация для корреляции. На практике достаточно нескольких карт map и аккуратного log_format, чтобы резко сократить риски утечек, не потеряв в эффективности эксплуатации. Начните с малого: уберите query string, огрубите реферер и обрежьте IP. Остальное можно наращивать по мере необходимости.


