Keeping secrets out of logs (2024)
Коротко:
Секреты в логах — это не «одним фиксом» решить нельзя. Ни 80/20, ни чудо-инструмента нет. Есть 10 «свинцовых пуль» — несовершенных, но при правильной раскладке работают.
Почему течёт
| Причина | Пример |
|---|---|
| Прямой логинг | log.info(user) вместо log.info(user.id) |
| «Мусорные» дампы | logger.debug(req.headers) |
| Конфиги | debug=true выводит весь env |
| Зашитые секреты | JSON-поле password внутри структуры |
| Телеметрия | APM-сборщик хватает всё подряд |
| Пользователь | Вводит пароль в поле «имя» |
10 «пуль»
-
Архитектура данных
Разделяем «чувствительное» и «остальное» на уровне схемы; в логи идёт только последнее. -
Трансформации
Сериализуем черезsanitize()илиtoLog()— явно выбрасываем секретные поля. -
Domain-primitives
- Компиляция:
SecretStringне реализуетDisplay. - Рантайм:
Redactableинтерфейс,toString() → "***".
- Компиляция:
-
Read-once
Пароль читается 1 раз, дальше объект пустой — логировать нечего. -
Taint-tracking
Помечаем вход как «грязный»; если доходит до логгера — exception. Дорого, но точно. -
Форматтеры логов
Пишем свойLayout/Encoder, который режет заранее заданные ключи рекурсивно. -
Unit-тесты
ПроверяемassertThat(log).doesNotContain(secret); запускаем на каждый PR. -
Сканеры
Regex-правила + entropy-фильтры в CI и в production-потоке. Сэмплируем, чтобы не умереть от CPU. -
Pre-processors
Vector / Logstash / Cribl вырезают поля ещё до попадания в Elasticsearch. -
Люди
Code-review чек-лист: «есть ли тут .toString / JSON.stringify / printf без фильтров?».
Стратегия
- Фундамент: классификация данных, единый словарь «что считать секретом».
- Карта потока: от источника до хранилища логов.
- Контрольные точки: валидация, sanitize, redact.
- Защита в глубину: 2-3 слоя из списка выше.
- План на инцидент: ротация, оповещение, forensics.
Итог:
Нет волшебства — только дисциплина и много мелких фиксов. Начните с 2-3 «пуль», которые дешёвле всего у вас, и двигайтесь дальше.
Комментарии (42)
- Отличный пост: чёткий разбор проблемы «секреты в логах» и конкретные техники борьбы.
- Основные идеи: taint-tracking, in-band метки, GuardedString/SecureString, доменные примитивы
new Secret(...). - Сложности: стектрейсы, JSON, core-dumps, динамически создаваемые секреты, человеческий фактор.
- Защита в глубину: маскировать, ограничивать доступ к логам, не писать всё подряд, валидировать маски (Kingfisher).
The key points of "Working Effectively with Legacy Code"
Краткий конспект «Working Effectively with Legacy Code»
«Легаси-код — это код без тестов»
(М. Фезерс, 2004)
Алгоритм работы
- Тесты → изменения
Сначала покрой тестами, потом трогай логику. - Парадокс легаси
Чтобы добавить тесты, надо изменить код; чтобы изменять, нужны тесты.
Решение: минимальные безопасные рефакторинги.
4 шага к тестам
- Найти шов (Seam) – точку, где можно подменить поведение без правки исходника.
Пример: унаследовать класс и переопределить метод. - Разорвать зависимости (БД, сеть, файлы).
- Написать быстрый (< 100 мс) изолированный тест.
- Вносить изменения и рефакторить.
Характеризационные тесты
Если логика не ясна, пишем тест, который фиксирует текущее поведение; потом рефакторим.
Комментарии (67)
- Книга «Working Effectively with Legacy Code» М. Фезерса вызывает спор: кому-то она дала язык и инструменты, кому-то показалась тривиальной и неприменимой к реальному аду из VB.NET, COBOL, VBA, AS/400 и прочим диалектам.
- Главная идея «напиши тесты, потом трогай» часто невозможна: требований нет, классов нет, тест-фреймворка нет, а босс слышит «рефакторинг» как «тратить деньги впустую».
- Поэтому практики делятся на два лагеря: «сначала покрой тестами хотя бы то, что трогаешь» и «запусти новую систему параллельно, сравнивай выходы, переключайся кусочками (Strangler Fig)».
- UI, скрипты и «макро-ассемблеры» не поддаются юнит-тестам; тут спасают визуальные диффы, сторибук, продовые снимки и осторожный ручной прогон.
- Рефакторинг превращается в бесконечное «yak-shaving», если каждый шаг не привязан к новой фиче или бизнес-ценности; политика и мотивация команды важнее любой методики.
Zig Error Patterns
Введение
Я часто использую отладчик, но привык и к выводной отладке, особенно в юнит-тестах. Хотелось улучшить её и чаще подключать отладчик.
Улучшение выводной отладки
Главная проблема — «шум»: в цикле интересна одна итерация, а печатается всё. Или удобнее читать форматированную структуру, но приходится раскидывать print’ы по коду. В Zig тесты используют error’ы, значит можно печатать только при падении теста через errdefer:
test { errdefer std.debug.print("{f}", .{ast}); // ... }
Так контекст появляется только при ошибке, без засорения лога.
Запуск тестов в отладчике
Просто запустить seergdb или gdb -tui неудобно: тестовые бинарники лежат в zig-cache. Трюк из ziggит: build.zig может запускать команды и передавать путь артефакта:
// seergdb — GUI фронтенд для gdb const debugger = b.addSystemCommand(&.{ "seergdb", "--run", "--" }); debugger.addArtifactArg(exe_unit_tests);
const debug_step = b.step("debug", "Run unit tests under debugger"); debug_step.dependOn(&debugger.step);
Это запускает правильный бинарник. Но отладчик сработает лишь на брейкпоинте или панике, тогда как раннер тестов «проглатывает» ошибки.
Комбинация трюков
Добавим @breakpoint через errdefer:
test { errdefer @breakpoint(); }
Так мы попадаем в точку ошибки, видим контекст и вывод std.testing.expect*. Минус: при zig build test отчёт показывает падение всего шага тестов, а не отдельных кейсов. Нужна возможность включать брейкпоинты выборочно.
Условная компиляция
Через build options пробрасываем флаг, решающий, вызывать ли @breakpoint в тестах.
Минимальный скрипт сборки, запускающий тесты, дополняем опциями:
const std = @import("std");
pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{});
const lib = b.addModule("zig-test-patterns", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const options = b.addOptions();
options.addOption(bool, "debugger", false);
lib.addImport("config", options.createModule());
const mod_tests = b.addTest(.{ .root_module = lib });
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}
В коде тестов:
const std = @import("std"); const config = @import("config");
test "errdefer @breakpoint()" { errdefer if (config.debugger) @breakpoint(); return error.FixMe; }
test "no breakpoint" { return error.FixMe; }
zig build test — без брейкпоинтов. Но менять значение флага так — значит пересобирать build.zig. Добавим опцию прямо в систему сборки:
var options = b.addOptions(); const use_debugger = b.option( bool, "debugger", "Enables code intended to only run under a debugger", ) orelse false; options.addOption(bool, "debugger", use_debugger);
Теперь можно переключать поведением командой:
zig build -Ddebugger test
И, при желании, привязать шаг запуска отладчика к этому флагу.
Комментарии (46)
- Участники хвалят согласованность базовых конструкций Zig: минимализм синтаксиса и мощь comptime позволяют элегантные решения без излишней сложности.
- Особый интерес вызвал errdefer: многие отмечают, что это упрощает тесты и отладку; звучит мнение, что такую возможность «стоит иметь каждому языку».
- Обсуждают практики отладки: полезны советы по интеграции дебаггера в build.zig, что избавляет от ручного поиска исполняемого файла в кэше.
- Поднимается вопрос об ошибках без полезной нагрузки в Zig: при парсинге (например, JSON) типовые ошибки вроде UnexpectedToken недостаточно информативны; интересуются паттернами передачи дополнительного контекста.
- Есть замечание о смешении стилей именования (camelCase в stdlib vs snake_case у автора), что может сбивать с толку.
- Отмечают эстетику сайта и блога: шрифты (Berkeley Mono), цветовую схему и ретро-оформление — «как в старых DOS-играх».
- Проводится параллель с D: аналогичная идея реализована через scope(failure), что подчеркивает общность концепции.