DarkRiDDeR16 мин

Разбор: токен попал в лог — как провести ротацию без ложного исправления

БезопасностьDevOps

Симптом учебного incident: обработчик 500-го ответа сериализовал заголовки запроса, и в централизованном логе появилась строка Authorization. Через несколько минут её заметили в поиске по логам. Цена — не только удаление одного поля. Значение могло попасть в alert, экспорт поддержки или сохранённый debug-ответ; пока старый credential действует, у команды нет права считать проблему закрытой.

Самая опасная реакция — быстро замаскировать свежий лог и написать «готово». Причина не устранена: нужно понять, где значение было создано, кто его потребляет, можно ли выпустить замену без простоя и как отозвать старое. В феврале 2020 года это можно разобрать без легенды о большой security-платформе: короткая карта фактов, смена credential у провайдера, обновление delivery и проверка всех зависимых процессов.

Сначала фиксируем факты, не копируя секрет ещё раз

В карточку incident не вставляю сам token, даже частично. Достаточно записать имя переменной, тип credential, момент обнаружения, носитель, предполагаемых потребителей и ссылку на закрытый безопасный канал владельца. Если значение уже видно в логе, дополнительная пересылка в issue расширяет круг читателей и создаёт ещё одну retention policy, которую потом придётся учитывать.

Учебная карта ротации: что должно остаться после каждого шага
ЭтапОтветственный за действиеБезопасный артефактЧто блокирует переход
Обнаружение и ограничениеДежурный разработчик или владелец сервисаID incident, имя credential, время, носительНепонятно, активен ли старый credential и где он виден
Новая пара и deliveryВладелец credential и deploy ownerID новой версии, список потребителей без valuesНе все consumers готовы читать новую пару
ПереключениеВладелец каждого процессаПроверка зависимого сценария и redacted startup recordЕсть consumer со старым значением или без owner
Отзыв старогоВладелец у провайдераПодтверждение revoke и времяНовая пара не проверена
Очистка следов и профилактикаВладелец репозитория и logging pathСписок носителей, тест redaction, follow-up taskУдаление строки выдано за отзыв credential

Таблица разделяет роли сознательно. Тот, кто видит ошибку, может не иметь права отозвать ключ у внешнего поставщика. Тот, кто создаёт replacement, может не знать всех воркеров, которые читают старую переменную. Когда этапы смешаны, команда либо отзывает credential слишком рано и создаёт простой, либо ждёт бесконечно, потому что никто не ведёт переключение. Карта делает неопределённость наблюдаемой до необратимого шага.

Вертикальная схема ротации: обнаружить и ограничить распространение, создать замену, обновить потребителей, отозвать старое значение, проверить следы и добавить защиту.
Ротация — это последовательность зависимых действий, а не commit с удалённой строкой. Старый credential отзывают только после проверки новой поставки или согласованного окна простоя.

Ограничиваем новую утечку до ротации

Первое действие — остановить дальнейшее распространение. Для учебного случая это значит убрать сериализацию headers из error path, ограничить доступ к конкретному поисковому запросу и сообщить владельцу credential. Не нужно сносить все логи или удалять проект: расследованию пригодятся время, request ID и версия сервиса, но они должны жить в разрешённом контуре. Ценность события — в фактах, а не в копии секретной строки.

Следующий быстрый check — найти все очевидные потребители по имени, а не по значению: backend service, worker, локальная инструкция, job delivery и тестовый контур. Поиск реального token в чатах или массовых логах может сам стать новой утечкой. Если нельзя связать имя с потребителем, эту неизвестность фиксируем как риск и не называем смену законченной.

Правим diagnostic path отдельным маленьким change

Логгеру не нужно угадывать, что каждый header безопасен. В обучающем примере имя заголовка проходит через allow/deny правило, а потенциально чувствительные поля получают одну и ту же маску. Реальный проект может иметь другой HTTP-клиент и другой формат логов; проверяемая идея одна: test должен доказывать, что в диагностическом объекте нет исходного значения.

function redactHeaders(headers) {
  const result = {};
  for (const [name, value] of Object.entries(headers)) {
    result[name] = /authorization|token|secret|password/i.test(name)
      ? "[REDACTED]"
      : value;
  }
  return result;
}

const sample = {
  authorization: "Bearer DEMO_NOT_A_REAL_TOKEN",
  requestId: "sample-2020-02",
};

redactHeaders(sample);
// { authorization: "[REDACTED]", requestId: "sample-2020-02" }

Строка DEMO_NOT_A_REAL_TOKEN намеренно фиктивна. Она проверяет форму результата, а не доступ к внешней системе. У полезного test есть два исхода: authorization заменяется на [REDACTED], а requestId остаётся, чтобы support мог связать запись с incident. Если test печатает sample целиком до вызова redaction, он не выполняет задачу — утечка уже случилась в самом тестовом выводе.

В такой change не стоит одновременно «улучшать» всю observability. Нужен узкий diff: безопасная функция или сериализатор, fixture с явно ненастоящим значением и один отрицательный test. Затем отдельная проверка должна посмотреть, не обходит ли другой error path этот serializer. Иначе новая маска создаст уверенность, а второй обработчик продолжит писать credential без защиты.

Ротация — это change с зависимостями

Порядок зависит от провайдера credential. Если он допускает две активные пары, сначала создаём новую, доставляем её всем известным consumers, проверяем сценарий, а затем отзываем старую. Если пара не может существовать одновременно, заранее выбираем короткое окно: останавливаем потребителя, меняем значение, запускаем проверку и фиксируем простой. Нельзя обещать бесшовность там, где provider её не гарантирует.

После обновления каждый consumer подтверждает только безопасный результат: название новой версии или время смены, успешный запрос в разрешённом тестовом сценарии, отсутствие старого имени в действующем config contract. Ни один из этих сигналов не требует передать token в issue. Если потребитель не может подтвердить смену, у него либо нет наблюдения, либо нет владельца; оба случая нужно закрыть до revoke.

Удаление из Git и из лога не равно отзыву

Git ignore помогает предотвратить добавление нового неотслеживаемого файла, но не удаляет уже tracked путь. Аналогично новый commit, который маскирует поле, не делает старое значение недействительным. GitHub в руководстве по утёкшим credentials отдельно ставит отзыв и выпуск замены раньше уборки repository: пока provider принимает старую пару, историческая или логовая копия остаётся рабочим риском.

Очистка истории и retention логов может быть нужной, но это согласованная операция с владельцем репозитория, хостинга и backup policy. В учебном сценарии я не предлагаю переписывать основную ветку или удалять записи вслепую. Сначала создаём новую рабочую пару, переключаем процессы и фиксируем revoke. Затем команда оценивает, какие копии ещё доступны и какой именно процесс уборки поддерживает её хостинг.

Маршрут incident от сигнала до критерия готовности

  1. Создать закрытую запись incident с именем credential, временем, носителем и владельцами; само значение не копировать.
  2. Остановить новый поток утечки: исправить serializer или логгер, сузить доступ к найденной записи и сохранить безопасные диагностические ID.
  3. Собрать список consumers по имени переменной и назначить владельца каждому. Не считать локальную инструкцию или worker неважным только потому, что он редко запускается.
  4. Согласовать с провайдером способ замены: параллельная новая пара либо окно переключения. Создать replacement через разрешённый канал, не через commit.
  5. Доставить новую конфигурацию каждому consumer и выполнить его узкую функциональную проверку. В записи оставить ID версии и результат, но не value.
  6. Отозвать старое значение у провайдера после проверки всех известных consumers либо в согласованное окно простоя.
  7. Проверить repository, image, CI log и logging path на следы прежней схемы; историю и retention чистить отдельной согласованной задачей.
  8. Добавить regression test redaction, шаблон без values и follow-up на неизвестных consumers. Закрыть incident только с подтверждением revoke и результатом проверок.

Что считается завершением, а что нет

Результат достаточен, когда старая пара отозвана, новая конфигурация проверена каждым известным consumer, error path маскирует соответствующие поля и у incident есть список неразрешённых копий либо подтверждение их обработки. Нет оснований писать, что «утечки не было»: команда может знать только носители, которые успела проверить. Это честная граница вывода и причина оставить follow-up, если лог-архив или старый clone требует отдельного владельца.

Этот разбор не выполняет ротацию реального сервиса, не открывает provider portal и не проверяет production. Он показывает форму безопасной работы: не размножать value в расследовании, не путать cleanup с revoke, не скрывать неизвестного consumer и добавлять контроль в точке, где diagnostic path раньше показал секрет. Для следующей команды это полезнее, чем один раз удалить строку и надеяться, что похожая ветка кода не вернётся.

Проверяемые источники

  • GitHub Docs: remediating a leaked secret — удаление строки не заменяет отзыв и выпуск новой учётной пары; затронутые потребители требуют отдельной проверки
  • Git: gitignore documentation — игнорируются только намеренно неотслеживаемые пути; уже tracked файл правило не убирает из индекса
  • Docker Docs: Dockerfile reference — ENV сохраняется в образе и доступен контейнеру; ARG не следует считать местом для credentials или токенов
  • Node.js v12: process.env — Node читает окружение процесса через process.env; это вход runtime, а не схема валидации сама по себе