Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 18 из 25 ~18 мин +50 XP

Секреты: маскирование и ПДн

Токен в логе живёт дольше, чем в памяти дежурного: запись уезжает в файл, оттуда в сборщик, оттуда в архив, где пролежит месяцы до ротации. Поэтому вопрос «что писать в логах» — это вопрос о самой долгоживущей копии ваших данных. Zerolog печатает значения полей как есть: метод Str не смотрит, похожа ли строка на пароль, и никакой встроенной фильтрации секретов в библиотеке нет — это осознанный контракт, а не недоработка, и защиту придётся строить на своей стороне вызова. Сам сайт ГоМастер персональных данных не собирает, и этот урок держится того же принципа: в примерах нет ни одного имени, телефона или адреса почты — только обезличенные идентификаторы вида user_id, как и должно быть в логах настоящего сервиса. Разберём три категории: то, что даёт доступ — токены, пароли, заголовок Authorization, куки сессий; то, что идентифицирует человека — фамилии, телефоны, почта, всё то, что закон о персональных данных называет ПДн; и то, что маскировать можно осмысленно — длинные идентификаторы, где первые и последние символы помогают корреляции.

Содержание урока · 10 разделов

Что скрываем всегда: доступ и ПДн#

Категория доступа — самая чувствительная, потому что утечка здесь равна передаче ключей. Пароль не логируется никогда, даже маскированным: маска выдаёт длину и крайние символы, а для перебора это подсказка, а не защита. Токен доступа — маскируется: две первые и две последние литеры дают возможность сопоставить записи между собой, не раскрывая значения. Заголовок Authorization пишется только схемой — Bearer без самого значения; если хотите показать, что аутентификация была, поле auth со значением bearer стоит дешевле и безопаснее. Кука сессии заменяется session_id — идентификатором без подписи, по которому сессию не восстановить. Строка запроса из урока про HTTP-логи — отдельная ловушка: r.URL.RawQuery регулярно тащит токены сброса пароля и приглашений, поэтому по умолчанию логируется только путь; у этого правила длинная история, и оно разобрано в уроке про HTTP-логи. Категория ПДн — фамилия, имя, телефон, почта, адрес, паспорт: то, что 152-ФЗ называет персональными данными. Если такие значения осели в логах, архив становится хранилищем ПДн со всеми требованиями закона: основание обработки, уровни защищённости, сроки, реакция на запросы субъектов. Требование «удалите всё обо мне» в неизменяемом лог-архиве технически невыполнимо — дешевле, чтобы там этого никогда не было.

Маска значения до вызова Str

GO
package main

import (
        "os"

        "github.com/rs/zerolog"
)

func mask(s string) string {
        r := []rune(s)
        if len(r) <= 4 {
                return "***"
        }
        return string(r[:2]) + "***" + string(r[len(r)-2:])
}

func main() {
        logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

        logger.Info().
                Str("user_id", "u-1024").
                Str("contact", mask("anna@example.com")).
                Str("token", mask("ya29.aBcDeFgHiJkLmNoP")).
                Msg("login ok")
}

// {"level":"info","time":"...","user_id":"u-1024",
//  "contact":"an***om","token":"ya***oP","message":"login ok"}

Маска режет строку по рунам, а не по байтам: кириллическая фамилия, разрезанная по байтам, дала бы битые символы в JSON. Значения до четырёх рун закрываются целиком — из двух литер нечего сохранять для корреляции. Маска применяется до вызова Str: в буфер события уже уходит готовая строка, и после записи её не изменить.

Маскировать до Str: почему после — поздно#

Механика поля объясняет, почему хук не спасёт. Вызов Str дописывает в буфер события готовые JSON-байты немедленно — ключ, кавычки, значение; никакого промежуточного представления, которое можно было бы отредактировать, не существует. Хук получает событие уже после ваших полей: его Run вызывается в момент Msg, когда буфер собран, а сам буфер — неприватное поле структуры, снаружи пакета zerolog его не прочитать. Доступные хуку операции — дописать собственные ключи в конец и отменить запись целиком; переписать или удалить чужое поле он не может технически. Поэтому маскирование живёт на стороне вызывающего кода, там, где про значение всё известно: функция mask рядом с местом, где строка появилась, а лучше — хелпер уровня пакета, единственная точка, через которую контакт или токен вообще попадает в лог. Такой хелпер заодно решает проблему забывчивости: если в кодовой базе нет ни одного прямого Str с контактным полем, маскировать нечего — точка входа одна, и она уже маскирует. Ключи при этом держите в константах, как договаривались в уроке про поля события: строковый литерал, продублированный в десяти местах, — это десять шансов опустить маску.

Хук-фильтр: интерфейс Hook и Discard#

Хук — интерфейс из одного метода: Run(e *Event, level Level, message string). Логгер принимает хуки методом Hook, который, как и With, возвращает копию — value-семантика, копию можно свободно раздавать горутинам. Выполняются хуки в момент финализации, при Msg, Msgf или Send, и раньше, чем message дописывается в буфер: хук видит текст сообщения параметром, а не полем. Отсюда честные сценарии. Вентиль: если в тексте сообщения встретилось слово, которого в логах быть не должно, событие отбрасывается целиком — e.Discard() помечает событие отключённым, и write пропускает его без записи; это ровно то, что написано в док-комментарии: Discard отключает событие, чтобы Msg его не напечатал. Дозапись: хук добавляет общие поля — имя окружения, версию сборки, — они ложатся после ваших полей и до message. Маркер: событие, прошедшее гейт, хук может пометить флагом, чтобы на стороне сбора отличать отфильтрованное. Для точечной настройки есть LevelHook и адаптер HookFunc из обычной функции — гейт вешается только на Debug, не трогая остальные уровни. Несколько хуков на одном логгере выполняются в порядке добавления, и каждый вызов Hook возвращает новую копию: собирайте набор хуков один раз на старте, вместе с контекстом и уровнем, а не в горячем пути.

Гейт на недопустимые сообщения

GO
package main

import (
        "os"
        "strings"

        "github.com/rs/zerolog"
)

type SecretGate struct{}

func (SecretGate) Run(e *zerolog.Event, level zerolog.Level, msg string) {
        if strings.Contains(strings.ToLower(msg), "password") {
                e.Discard()
        }
}

func main() {
        logger := zerolog.New(os.Stdout).Hook(SecretGate{}).With().Timestamp().Logger()

        logger.Info().Str("op", "login").Msg("user signed in")
        logger.Info().Str("op", "reset").Msg("password change requested")
}

// {"level":"info","time":"...","op":"login","message":"user signed in"}
// вторая запись не появится: гейт отбросил событие целиком

Гейт проверяет текст сообщения в нижнем регистре и отбрасывает событие вызовом Discard: уровень становится Disabled, и write пропускает запись. Первая строка проходит — слова password в сообщении нет. Хук добавлен методом Hook до With, но порядок здесь не важен: и Hook, и With возвращают копию логгера, обе можно продолжать цепочкой.

Гигиена полей: чем заменять секреты#

Замена работает лучше запрета: вместо списка «нельзя» — список «пишем вместо этого». Пользователь — user_id: обезличенный идентификатор сшивает записи так же надёжно, как почта, и не является ПДн. Контакт — маской, и только там, где дежурному правда нужно опознать человека: разбор обращения в поддержку. Токен — маской ради корреляции записей одного клиента. Сессия — session_id без подписи. Карта — последними четырьмя цифрами: отраслевой стандарт, и для разбора инцидента этого достаточно. Ревью-чеклист короткий: посчитайте в диффе вызовы Str, чьё значение приходит из заголовков, форм или тел запросов; каждый такой вызов — кандидат в маскирование или в идентификатор. Следующий рубеж — тест: программа, которая пишет события в буфер и сканирует вывод на слова-маркеры вроде password, authorization, bearer; механика таких проверок — тема урока про тестирование логов. Последняя линия — правила маскирования на стороне сборщика: пайплайн умеет вырезать и заменять значения уже после записи. Это лечение симптома: строка покинула процесс, копии разъехались по узлам, и гарантий, что вычистили всё, нет. Сборщик смягчает аварию, но не отменяет дисциплину в коде.

Что скрываем и чем заменяем#

ЗначениеВ логПочему
Парольне пишется вовсемаска выдаёт длину и крайние символы
Токен доступапервые 2 + *** + последние 2края сохраняют корреляцию записей
Authorizationтолько схема: bearerсамо значение — ключ доступа
Кука сессииsession_idидентификатор без подписи не оживит сессию
Почта, телефонмаска или user_idПДн: архив логов не должен их содержать
Номер картыпоследние 4 цифрыотраслевой стандарт, для разбора достаточно
ФИОuser_idимя не нужно, чтобы понять, что случилось

Про 152-ФЗ с практической стороны. Закон требует основания для обработки персональных данных и защиты в зависимости от угроз; лог-архив, в котором осели фамилии и телефоны, формально становится местом обработки, о котором вы, скорее всего, ничего не декларировали. Обезличивание решает вопрос радикально: идентификатор user_id не позволяет восстановить человека без второй таблицы, а вторая таблица под замком и есть ваша база ПДн, о которой вы всё знаете. Отсюда вторая половина гигиены — доступ и срок жизни. Кто читает логи: доступ к архиву даётся тем же ролям, что и к базе, а не всем инженерам по умолчанию. Сколько живут записи: ротация из урока про lumberjack ограничивает возраст архива — месяц логов вместо бесконечного накопления сужает окно утечки. Ретеншн и маскирование работают в паре: маска не даёт секрету попасть в архив, ретеншн не даёт архиву жить вечно. Комбинация «в логах нет ПДн, доступ ограничен, хранение ограничено» закрывает типовые вопросы проверки почти без правок в приложении: всё это настройки writer-а и инфраструктуры.

Правила гигиены одним списком#

  • пароль не логируется никогда — даже маской: маска выдаёт структуру значения
  • токен и контакт — маской до вызова Str: две первые и две последние литеры
  • Authorization — только схемой; RawQuery — только осознанно, по умолчанию путь
  • человек в логе — это user_id, а не фамилия и не почта
  • ключи чувствительных полей — в константах пакета, запись — через один хелпер
  • тест сканирует вывод лога на слова-маркеры: password, authorization, bearer
  • правило на сборщике — последняя линия, а не замена дисциплине в коде
  • хук умеет дописать поле или Discard — читать и править поля он не может

Внедрение — вопрос команды, а не одного энтузиаста. Хелпер и константы живут в пакете логирования, и правило звучит просто: чувствительное значение попадает в лог только через него; прямой Str с токеном в ревью — блокер пул-реквеста. Тест на слова-маркеры запускается в CI рядом с юнит-тестами: если кто-то протащил сырой заголовок, сборка краснеет до того, как код доедет до продакшена. Дежурному стоит знать и процедуру на случай находки: секрет в логе — это инцидент утечки, и лечится он ротацией значения, а не правкой записи. Токен отзывается и выдаётся новый, пароль меняется, сессия инвалидируется; стереть строку из уже собранного архива нельзя, и пытаться «поправить файл» бессмысленно — копии уже разъехались по узлам. Для онбординга полезен живой пример: новичок запускает демо с настоящим токеном в поле, видит его в ленте в чистом виде, затем повторяет с маской — контраст запоминается лучше памятки. Гигиена держится не запретами, а дешёвым правильным путём: когда маскировать так же просто, как не маскировать, маскируют все.

Проверка: значения полей

Что сделает zerolog при вызове Str("token", "abc123") с настоящим токеном?

Проверка: возможности хука

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

Что вернёт mask?

Прочитайте код и выберите, что он выведет:

mask("ab")
mask("ABCDEFGH")

Гейт на пароль

Восстановите хук-гейт и его подключение к логгеру.

func (SecretGate) Run(e *zerolog.Event, level zerolog.Level, msg string) { if strings.(strings.ToLower(msg), "password") { e.() } } logger := zerolog.New(os.Stdout).(SecretGate{}).Logger()

Программа с маской

Расставьте строки в правильном порядке: импорты, функция маски и запись с замаскированным значением.

Строки

Ваша программа

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

Карточки: секреты и ПДн

Термин

ПДн в логах

нажмите, чтобы перевернуть
Определение

Фамилия, телефон, почта, адрес — персональные данные по 152-ФЗ. В логах их быть не должно: человек представлен обезличенным user_id.

1 / 6

Что дальше по курсу#

Следующий урок — структура JSON: имена level, time и message, вложенность через Dict и Array и почему системе сбора нужна плоская структура. Затем — тесты логов: буфер вместо writer-а и сканирование вывода на слова-маркеры из этого урока. Про то, как выглядит доступ и хранение на настоящем сервере, — логи на проде: роли читателей, ретенция и то, что из этого урока превращается в настройки инфраструктуры.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Zerolog не маскирует ничего: значения полей уходят в JSON как есть.
  • Пароль не пишется никогда; токен и контакт — маской до вызова Str.
  • Маска: две первые и две последние литеры, короче — целиком закрытая.
  • Хук — интерфейс Run(e, level, message): дописать ключи или Discard, не больше.
  • Хук не читает и не переписывает добавленные поля: буфер неприватный.
  • Человек в логе — user_id, а не фамилия: архив логов не хранилище ПДн.
  • Тест на слова-маркеры и ретеншн: дисциплина в коде плюс ограниченный срок хранения.

Домашнее задание

Возьмите middleware из урока про HTTP-логи и проведите аудит его полей: найдите места, где в лог могут уехать RawQuery, заголовки или тело запроса. Напишите функцию mask по правилу урока — руны, порог в четыре литеры — и повесьте её на все строковые поля, чьи значения приходят от клиента. Добавьте хук-гейт, отбрасывающий события с password в сообщении, и проверьте его двумя записями: чистой и с запрещённым словом. Затем соберите тест, который пишет события в bytes.Buffer через SyncWriter и сканирует вывод на маркеры password, authorization и bearer — это будет ваш регресс на утечки. Напоследок посчитайте, сколько дней живут файлы логов в вашей конфигурации ротации, и решите, устраивает ли вас это окно.

Личные заметкиПусто
Хранятся только в вашем браузере (zerolog · Секреты: маскирование и ПДн)

Вопросы и ответы по уроку

Есть ли в zerolog встроенное маскирование секретов?

Нет. Библиотека печатает значения полей как есть и не имеет API для фильтрации или замены содержимого. Маскирование строится на стороне приложения: функция, которая укорачивает строку до первых и последних литер, вызывается до Str. Хук тоже не поможет переписать поле — он умеет лишь дописывать ключи и отменять событие через Discard.

Что может хук zerolog?

Интерфейс из одного метода Run(e *Event, level Level, message string), который вызывается при финализации события, до дописывания message. Хук видит текст сообщения параметром и может дописать свои поля в конец или вызвать Discard, отменив запись целиком. Читать и править уже добавленные поля он не может: буфер события — неприватное поле структуры.

Что делать с персональными данными в логах по 152-ФЗ?

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

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Validator25 уроков~11 чпрактика25 уроков уже доступны

Валидация в Go: go-playground/validator

Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.

  • Валидировать структуры тегами binding и validate
  • Понимать ValidationErrors и писать понятные сообщения
  • Работать с вложенностью, срезами и картами через dive
  • Писать свои функции-валидаторы и StructLevel-правила