MAATRIX / Блог / Антипаттерн: токены и персональные данные в логах

Антипаттерн: токены и персональные данные в логах

MAATRIX

Вы закрыли доступ к базе данных, включили шифрование дисков, настроили ротацию паролей — а рядом лежит /var/log/app/app.log с открытым для всей команды доступом, куда приложение годами пишет заголовок Authorization, тело POST-запроса с паролем и email каждого пользователя. Логи почти никогда не входят в периметр защиты, который вы строите вокруг основной базы, хотя по факту содержат те же самые секреты. Это один из самых недооценённых антипаттернов — и один из самых простых в устранении, если знать, куда смотреть.

Как секреты и ПДн оказываются в логах

Утечка в логи почти никогда не результат злого умысла — это побочный эффект отладки, который забыли выключить. Разберём самые частые пути.

Логирование сырого тела запроса и ответа. Классика: миддлварь логирует request.body и response.body целиком для отладки — и туда попадает JSON с полем password, card_number или refresh_token. Пример на Express:

app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`, JSON.stringify(req.body));
  next();
});

Если req.body — это форма логина или обновления профиля, в лог улетает пароль в открытом виде. То же самое с ответами: если API возвращает объект пользователя с телефоном и адресом, а вы логируете res.body для отладки — эти данные оседают в файле, который никто специально не защищал.

Логирование заголовков целиком. Разработчик добавляет console.log(req.headers) при отладке авторизации и забывает убрать. В заголовках лежат Authorization: Bearer …, Cookie: session=…, иногда X-Api-Key. Один такой console.log, оставленный в проде на несколько месяцев, — это тысячи токенов в открытом файле.

Логирование SQL-запросов с параметрами. ORM в debug-режиме печатает полный SQL с подставленными значениями:

DEBUG: UPDATE users SET password_hash='$2b$12$xJ3k...', email='ivan@example.com' WHERE id=42

Даже хэш пароля — чувствительная штука для лога с широким доступом, а если по ошибке логируется сырой пароль до хэширования (бывает при отладке валидации) — это прямая утечка.

Логирование ошибок с полным контекстом. Обработчик исключений печатает весь объект запроса или сессии при ошибке — «для контекста». В стектрейсе оказывается токен, которым пользователь был авторизован в момент падения.

Сторонние библиотеки и APM. APM-агенты и error-tracking SDK по умолчанию иногда захватывают полное тело запроса и заголовки, отправляя их в облачный сервис — данные покидают не только ваш сервер, но и юрисдикцию, если сервис хостится за рубежом без DPA.

Логи сборки и CI/CD. Отдельная категория: секреты попадают не в лог приложения, а в лог сборки — переменные окружения печатаются в вывод docker build или CI-джобы. Разбирали на примере утечки секрета в лог сборки — там же цена шестичасового окна до ротации ключей.

Почему это опаснее, чем кажется: логи не защищены как БД

Здесь и кроется суть антипаттерна: команда вкладывается в защиту основной БД (шифрование, сетевые ограничения, отдельные учётки), но логи остаются на обочине этой модели угроз — хотя по содержимому они не менее чувствительны.

Логи живут дольше данных. Часто ротация настроена на хранение 30, 90 или 180 дней «на всякий случай для расследований» — токен, попавший в лог в январе, доступен для чтения в марте, хотя в БД соответствующая сессия давно истекла.

Доступ к логам шире, чем к БД. У доступа к продовой базе обычно строгий список: два-три инженера, отдельные учётки, VPN. К логам же часто имеет доступ вся команда разработки — «чтобы отлаживать баги», плюс саппорт, плюс иногда подрядчики. Чем шире круг людей с доступом к файлу с токенами и паролями, тем выше вероятность утечки — случайной (скопировали лог в тикет для support) или намеренной.

Логи копируются бесконтрольно. Разработчик берёт кусок лога с продакшена, чтобы приложить к баг-репорту в Jira или скинуть в чат команде — и там оказывается фрагмент с чужим email, номером телефона или, того хуже, токеном авторизации. В отличие от доступа к БД, который обычно требует отдельного логина и оставляет след, копипаст строки из лога в мессенджер никто не аудирует.

Логи попадают в бэкапы без отдельного шифрования. Если бэкап сервера включает /var/log целиком, а бэкапы лежат у стороннего провайдера или в облаке без дополнительного шифрования — токен из лога полугодовой давности может утечь через компрометацию бэкапа, а не самой БД.

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

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

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

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

Комплаенс: что говорит закон о ПДн в логах

Если приложение обрабатывает персональные данные пользователей (email, телефон, адрес, платёжные реквизиты), то лог, где эти данные лежат в открытом виде, — это тоже база персональных данных с точки зрения регуляторики, даже если вы никогда не думали о нём в этих терминах.

Требование минимизации. GDPR-подобные нормы (включая российский 152-ФЗ) требуют обрабатывать не больше персональных данных, чем нужно для цели. Цель логирования — диагностика и отладка, а не хранение персональных данных пользователей. Если для диагностики достаточно user_id, логировать email и телефон рядом с ним — избыточно и формально нарушает принцип минимизации.

Право на удаление. Если пользователь запросил удаление своих данных, а они размазаны по десяткам файлов логов за полгода на нескольких серверах — вы физически не можете обеспечить их удаление в разумный срок. Структурированные логи с ПДн без стратегии удаления создают юридический риск, который не решается на уровне «мы же не БД».

Передача данных за рубеж. Если логи или события уходят в SaaS для мониторинга/трейсинга, размещённый вне вашей юрисдикции, — с точки зрения регуляторики это трансграничная передача персональных данных, требующая отдельных оснований (согласие, DPA с обработчиком). Многие команды об этом не задумываются, включая APM по умолчанию с полным захватом тела запроса.

PCI DSS для платёжных данных отдельно жёсток. Логирование полного номера карты (PAN) в любом виде, включая логи ошибок платёжного шлюза, — прямое нарушение PCI DSS. Требование: хранить максимум первые 6 и последние 4 цифры, остальное маскировать, независимо от того, лог это или БД.

Отдельно про закон и логи есть разбор — что можно и нужно хранить в логах по закону.

Как маскировать чувствительные поля перед логированием

Правильный уровень борьбы с этим антипаттерном — не «запретить логировать», а обеспечить, чтобы чувствительные поля никогда не попадали в лог в исходном виде.

Список полей на редактирование (redact list). Большинство современных логгеров поддерживают декларативный список путей, которые нужно маскировать автоматически. Пример на Node.js с pino:

const pino = require('pino');
const logger = pino({
  redact: {
    paths: [
      'req.headers.authorization',
      'req.headers.cookie',
      'req.body.password',
      'req.body.card_number',
      'user.email',
      'user.phone',
    ],
    censor: '[REDACTED]',
  },
});

Аналог на Python (structlog / logging filter):

import logging

SENSITIVE_KEYS = {"password", "token", "authorization", "card_number", "email"}

class RedactFilter(logging.Filter):
    def filter(self, record):
        if isinstance(record.args, dict):
            record.args = {
                k: "[REDACTED]" if k.lower() in SENSITIVE_KEYS else v
                for k, v in record.args.items()
            }
        return True

logger = logging.getLogger("app")
logger.addFilter(RedactFilter())

Маскирование вместо полного вычёркивания там, где нужна корреляция. Иногда для отладки важно понимать, что несколько записей относятся к одному пользователю, но сам email или телефон в логе не нужен. Тогда вместо [REDACTED] используйте частичную маску или хэш:

def mask_email(email: str) -> str:
    name, domain = email.split("@")
    return f"{name[0]}***@{domain}"

def mask_pan(card: str) -> str:
    return card[:6] + "*" * (len(card) - 10) + card[-4:]

Хэш (например усечённый SHA-256 от user_id) хорош там, где нужна возможность сопоставить записи одного пользователя между собой, но не нужно раскрывать сам идентификатор.

Отдельные уровни логирования для тела запроса. Полное тело запроса, если оно вообще нужно для отладки, должно логироваться только на уровне debug и только в непродовом окружении. В проде — только метаданные: метод, путь, код ответа, длительность, ID пользователя (не email).

ПолеЧто логировать в проде
Пароль, токен, API-ключНикогда, даже частично
EmailМаска i***@domain.com или не логировать
Номер картыПервые 6 + последние 4 цифры, остальное — маска
ТелефонМаска последних цифр или не логировать
user_id, session_id (непубличный)Можно, если используется только для корреляции
IP-адресМожно, с учётом срока хранения по закону о ПДн

Аудит: что реально попадает в логи

Список правил редактирования бесполезен, если никто не проверяет, соблюдается ли он на практике. Аудит — это отдельный регулярный процесс, а не разовая настройка.

Ручной грепом по продовым логам. Простейшая и при этом эффективная проверка — периодически прогонять логи через grep по ключевым словам:

grep -riE "password|passwd|token|secret|api[_-]?key|authorization|card[_-]?number|cvv" /var/log/app/*.log | head -50

Если команда возвращает совпадения за пределами имён полей (то есть реальные значения, а не просто слово «password» в описании ошибки) — это сигнал чинить конкретное место в коде.

Сканирование логов инструментами поиска секретов. Утилиты вроде trufflehog или detect-secrets, которые обычно применяют к git-репозиториям, можно направить и на выгрузку логов:

trufflehog filesystem /var/log/app/ --json | jq '.DetectorName'

Это ловит структурные паттерны токенов (JWT, ключи облачных провайдеров и т.д.), которые простой grep пропустит из-за нестандартного имени поля.

Код-ревью с чек-листом по логированию. Добавьте в шаблон пул-реквеста пункт: «Если добавлено логирование — проверено, что в лог не попадают password/token/PII». Это дешевле, чем находить утечку постфактум, и приучает команду думать о логах как о поверхности риска, а не нейтральном дебаг-выводе.

Периодическая выборочная проверка новых полей. Раз в квартал стоит вручную просмотреть свежий срез логов (сотню-другую строк) на предмет новых полей, которые появились с обновлениями API и не попали в исходный redact-список — новые эндпоинты и интеграции часто добавляют логирование «по образцу», унаследовав чувствительные поля.

Если у вас уже настроен централизованный сбор логов (Graylog, Grafana Loki), аудит удобнее делать через поисковый запрос по индексу, а не построчным grep по файлам на каждом сервере — это быстрее и не требует захода на продовые машины.

Отдельная защита доступа к логам

Даже при идеальном маскировании часть контекстных данных (IP, user_id, метаданные запросов) неизбежно остаётся в логах — и доступ к ним нужно защищать так же осознанно, как доступ к БД, а не как к «обычным файлам сервера».

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

# Права на директорию логов только для сервисной группы, без чтения "всем"
chown -R app:logreaders /var/log/app
chmod 750 /var/log/app
find /var/log/app -type f -exec chmod 640 {} \;

Шифрование при передаче и хранении. Если логи отправляются на внешний коллектор (Loki, Graylog, ELK), убедитесь, что транспорт идёт по TLS, а не по открытому TCP/UDP-syslog внутри сети, которую вы не полностью контролируете.

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

# logrotate: хранить 30 дней, сжимать, удалять старое
/var/log/app/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
}

Не пересылайте логи в сторонние SaaS без проверки договора обработки данных. Если подключаете внешний сервис мониторинга или трейсинга, убедитесь, что он не получает сырые чувствительные поля — либо настройте маскирование на стороне агента до отправки, либо не подключайте продовый трафик с ПДн вовсе.

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

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

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

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

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

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

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

Если мы уже отредактировали чувствительные поля в новом коде, нужно ли чистить старые логи?

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

Достаточно ли просто не логировать req.body целиком?

Это снижает риск, но не закрывает его полностью — заголовки, параметры URL (?token=...), и SQL-запросы с параметрами тоже частый источник утечки. Нужен системный подход, а не одно правило.

Как быть с логами уровня debug, которые разработчики включают локально?

Локальная разработка обычно безопаснее (свои тестовые данные), но если конфиг debug-уровня случайно попадает в прод через переменную окружения — это тот же риск. Проверяйте на старте приложения, что debug-логирование выключено вне dev/staging явной проверкой окружения.

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

Полное шифрование диска (LUKS) закрывает риск физического доступа к серверу, но не риск чтения через легитимный доступ к файлу человеком без необходимости. Права доступа и аудит того, кто их использует, важнее, чем шифрование файла как таковое.

Как проверить, не логирует ли сторонняя библиотека что-то лишнее сама по себе?

Просмотрите документацию по умолчаниям для APM/трейсинг-агентов — многие по умолчанию захватывают полное тело запроса и требуют явного отключения (captureBody: false и подобные флаги). Не полагайтесь на предположение «наверняка не логирует».

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

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

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