---
title: "Секреты: маскирование и ПДн"
description: "Маскирование логов zerolog: token, password и Authorization, hook-фильтр с Discard, ПДн по 152-ФЗ и гигиена полей — что скрывать всегда и чем заменять в полях."
course: "Zerolog в Go: быстрое JSON-логирование"
url: "https://golangmaster.ru/courses/zerolog/lessons/18-log-polya-sekrety/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Zerolog в Go: быстрое JSON-логирование», урок 18 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/zerolog/lessons/18-log-polya-sekrety/*

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

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

Категория доступа — самая чувствительная, потому что утечка здесь равна передаче ключей. Пароль не логируется никогда, даже маскированным: маска выдаёт длину и крайние символы, а для перебора это подсказка, а не защита. Токен доступа — маскируется: две первые и две последние литеры дают возможность сопоставить записи между собой, не раскрывая значения. Заголовок Authorization пишется только схемой — Bearer без самого значения; если хотите показать, что аутентификация была, поле auth со значением bearer стоит дешевле и безопаснее. Кука сессии заменяется session_id — идентификатором без подписи, по которому сессию не восстановить. Строка запроса из урока про HTTP-логи — отдельная ловушка: r.URL.RawQuery регулярно тащит токены сброса пароля и приглашений, поэтому по умолчанию логируется только путь; у этого правила длинная история, и оно разобрано в [уроке про HTTP-логи](/courses/zerolog/lessons/11-log-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 с контактным полем, маскировать нечего — точка входа одна, и она уже маскирует. Ключи при этом держите в константах, как договаривались в уроке про поля события: строковый литерал, продублированный в десяти местах, — это десять шансов опустить маску.

> **Живое место: хук не раскрывает поля.** Если рассчитываете, что хук перепишет уже добавленное поле token, — не дождётесь: к моменту Run байты лежат в буфере, а сам буфер снаружи нечитаем. Хук умеет ровно две вещи — дописать свои ключи и вызвать Discard, отменив запись целиком. Если секрет уже уехал в 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; механика таких проверок — тема [урока про тестирование логов](/courses/zerolog/lessons/20-testy-logov/). Последняя линия — правила маскирования на стороне сборщика: пайплайн умеет вырезать и заменять значения уже после записи. Это лечение симптома: строка покинула процесс, копии разъехались по узлам, и гарантий, что вычистили всё, нет. Сборщик смягчает аварию, но не отменяет дисциплину в коде.

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

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

> **Живое место: Debug прячет, но не стирает.** Если секреты пишутся на Debug «временно, под уровнем», то в ночь инцидента порог опустят до debug — и всё уедет в архив вместе с токенами: уровень прячет запись из потока, но не делает её безопасной. Если значение нельзя показать на Info, его нельзя показывать вообще: маскируйте или не пишите — уровня, на котором можно всё, не существует.

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

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

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

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

> **Про песочницу.** В песочнице библиотека не запускается: программа урока разбирается построчно, а маскирование видно по комментариям с выводом. Локально всё повторяется терминалом: go run main.go на примере с гейтом — и запись с password в сообщении исчезнет из ленты, самый наглядный способ увидеть Discard в деле.

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

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

- ✅ Запишет значение в JSON как есть: проверки и маскирование — на стороне вашего кода
- Автоматически заменит значение на ***
- Отбросит событие: секреты запрещены на уровне библиотеки
- Сообщит об ошибке через ErrorHandler

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

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

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

- Прочитать поля и заменить значения секретов
- Удалить отдельные ключи из буфера
- ✅ Дописать свои поля или отменить запись целиком вызовом Discard
- Перезаписать message до записи в writer

Буфер события снаружи пакета нечитаем: хук получает непрозрачное событие и текст сообщения. Ему доступны дозапись ключей и Discard — уровень Disabled заставляет write пропустить запись.

### Что вернёт mask?

```go
mask("ab")
mask("ABCDEFGH")
```

**Что выведет программа?**

- ✅ *** и AB***GH
- ab***ab и AB***GH
- *** и ***
- ab и ABCD***EFGH

Две литеры — короче порога, целиком закрытая маска. Восемь рун — первые две и последние две вокруг трёх звёздочек: маска сохраняет края для сопоставления записей.

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

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

```go
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()
```

Подсказка: Поиск подстроки — Contains, отмена события — Discard, хук вешается методом Hook.

Правильные ответы в пропусках: `Contains`, `Discard`, `Hook`

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

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

Правильный порядок строк:

1. `package main`
2. `import (`
3. `  "os"`
4. `  "github.com/rs/zerolog"`
5. `)`
6. `func mask(s string) string {`
7. `  if len([]rune(s)) <= 4 { return "***" }`
8. `  return string([]rune(s)[:2]) + "***" + string([]rune(s)[len([]rune(s))-2:])`
9. `}`
10. `func main() {`
11. `  zerolog.New(os.Stdout).Info().Str("token", mask("abc")).Msg("login ok")`
12. `} // конец программы`

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

- **ПДн в логах** — Фамилия, телефон, почта, адрес — персональные данные по 152-ФЗ. В логах их быть не должно: человек представлен обезличенным user_id.
- **Маскирование до Str** — Значение маскируется функцией до вызова метода: Str дописывает байты немедленно, и после записи поле не изменить.
- **Интерфейс Hook** — Один метод Run(e, level, message). Логгер получает хуки методом Hook, который возвращает копию; выполняются при Msg до дописывания message.
- **e.Discard()** — Помечает событие отключённым: write пропускает запись целиком. Так хук-гейт убирает событие с недопустимым текстом.
- **Что хук не может** — Прочитать или переписать уже добавленные поля: буфер события неприватный. Только дозапись ключей и отмена записи.
- **Ретеншн и доступ** — Ротация ограничивает возраст архива, роли ограничивают круг читающих. Вместе с маскированием — базовая гигиена логов.

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

Следующий урок — [структура JSON](/courses/zerolog/lessons/19-log-formaty-json/): имена level, time и message, вложенность через Dict и Array и почему системе сбора нужна плоская структура. Затем — тесты логов: буфер вместо writer-а и сканирование вывода на слова-маркеры из этого урока. Про то, как выглядит доступ и хранение на настоящем сервере, — [логи на проде](/courses/zerolog/lessons/24-logi-na-prode/): роли читателей, ретенция и то, что из этого урока превращается в настройки инфраструктуры.

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

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

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

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

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

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

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

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

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

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

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