CodeAuditAgent
Все статьи
  • Безопасность
  • Git
  • Секреты

Утечка секретов в истории git: найти, заменить, предотвратить

Удаление закоммиченного API-ключа не стирает его из истории git. Как найти утёкшие секреты, сначала заменить их, безопасно переписать историю и не допустить новой утечки.

· Чтение: 7 мин · Lina Source LLC

Кто-то коммитит файл .env или вставляет рабочий API-ключ в модуль конфигурации, чтобы быстро что-то проверить. Ревьюер замечает это, ключ удаляют следующим коммитом, и все идут дальше. Но ключ никуда не делся. Git хранит каждую версию каждого файла, и любой, кто может клонировать репозиторий, прочитает коммит, в котором ключ был добавлен.

Жёстко прописанные в коде учётные данные классифицируются как CWE-798. В этом руководстве описано, что делать, если секрет уже попал в историю: найти все секреты, в первую очередь заменить их, решить, стоит ли переписывать историю, и настроить защиту, которая остановит следующую утечку.

Почему недостаточно удалить строку

Коммит, удаляющий секрет, добавляет новый снимок без него. Предыдущий снимок с секретом по-прежнему остаётся в истории ветки. git log -p покажет его, git checkout старого коммита восстановит его, а у каждого существующего клона и форка уже есть копия. Сквош ветки перед слиянием тоже не поможет, если исходные коммиты хоть раз были отправлены: они могут оставаться на удалённом сервере, а у всех, кто их получил, они есть локально.

Если репозиторий публичный, исходите из того, что секрет уже видели. Автоматические сборщики отслеживают публичные push на предмет шаблонов учётных данных, и между push и первым злоупотреблением может пройти очень мало времени. В приватном репозитории риск меньше, но не нулевой: секрет есть у каждого сотрудника, подрядчика, CI-системы и интеграции с правом чтения.

Шаг 1: сначала замена

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

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

  • Сгенерируйте новый ключ в консоли провайдера и разверните его через обычный путь доставки конфигурации.
  • Отзовите старый ключ. Недостаточно просто перестать его использовать: неиспользуемый действующий ключ всё равно остаётся действующим.
  • Проверьте журналы аудита провайдера на активность со старым ключом начиная с даты коммита: вызовы API, новые пользователи, изменённые права, неожиданные списания.
  • Если секретом был пароль базы данных или ключ подписи, подумайте, что он мог открыть. Утечка секрета подписи JWT означает, что любой токен мог быть подделан, поэтому аннулируйте существующие сессии.
  • Запишите, что было раскрыто, как долго и что вы изменили. Это пригодится, если спросит клиент или аудитор.

Шаг 2: найти всё, что утекло

Где есть один закоммиченный секрет, там часто есть и другие. Ищите по всей истории, а не только в текущем дереве, и включайте все ветки и теги.

# Коммиты, где строка была добавлена или удалена, по всей истории
git log -p -S "sk_live_" --all

# Поиск по регулярному выражению в диффах всех коммитов
git log -p -G "AKIA[0-9A-Z]{16}" --all

# Файлы, когда-либо существовавшие по подозрительному пути
git log --all --oneline -- .env config/production.json

# Специализированные сканеры проверяют сотни известных форматов учётных данных
gitleaks git -v .
trufflehog git file://. --only-verified

gitleaks и trufflehog — сканеры с открытым исходным кодом, созданные именно для этой задачи. Они знают форматы ключей распространённых провайдеров, а trufflehog умеет проверять, действуют ли ещё найденные учётные данные. В старых версиях gitleaks вместо подкоманды git используется gitleaks detect --source . Один раз прогоните сканер по всей истории, а затем запускайте его на каждом новом коммите. Ожидайте ложных срабатываний, например на тестовых фикстурах и примерах ключей в документации. Проверьте каждое, а затем внесите подтверждённые ложные срабатывания в allowlist или baseline-файл сканера, чтобы следующий запуск показывал только новые находки.

Не забывайте о местах вокруг репозитория: логи CI, в которые выводились переменные окружения, комментарии в issue и pull request, вики-страницы, слои Docker-образов, собранных из репозитория, а также gist и пасты, которыми делились при отладке.

Шаг 3: решить, переписывать ли историю

После ротации секрет бесполезен, поэтому переписывание истории — это уборка, а не локализация инцидента. Делать её всё же стоит, если репозиторий публичный, если секрет раскрывает что-то помимо себя (внутреннее имя хоста, имя клиента) или если этого требуют нормы соответствия. У неё есть реальная цена: каждому участнику придётся заново клонировать репозиторий, открытые pull request будут нарушены, а хеши коммитов изменятся. Всё, что ссылается на коммит по хешу, — заметки о релизах, записи о развёртываниях, ссылки в issue или закреплённые зависимости в других репозиториях — будет указывать на коммиты, которых в ветке больше нет. Объявите о переписывании заранее, выберите спокойный момент и сначала слейте или закройте открытые pull request.

Для этого проект Git рекомендует git filter-repo вместо старого git filter-branch. Он работает на свежем клоне и в целях безопасности удаляет remote origin, поэтому перед push его нужно добавить обратно.

# Начните со свежего клона
git clone [email protected]:acme/api.git api-clean
cd api-clean

# Вариант A: удалить файл из всех коммитов
git filter-repo --invert-paths --path config/production.env

# Вариант B: заменить строки секретов везде, где они встречаются
# replacements.txt содержит по одному правилу на строку, например:
#   PASTE_THE_OLD_KEY_HERE==>REDACTED
git filter-repo --replace-text ../replacements.txt

# filter-repo удаляет origin; добавьте его обратно и сделайте force-push
git remote add origin [email protected]:acme/api.git
git push --force --all
git push --force --tags

Чего переписывание не может

  • Оно не затрагивает существующие клоны, форки, кеши CI и резервные копии. У всех, кто получил данные до переписывания, старые коммиты остаются.
  • На GitHub ссылки, созданные для pull request, доступны только для чтения и могут сохранять достижимость старых коммитов. Чтобы удалить кешированные представления и эти ссылки, нужно обратиться в поддержку GitHub.
  • Участники, которые делают pull или merge из старого клона, могут отправить старую историю обратно. Попросите всех заново клонировать репозиторий и защитите ветку на это время.
  • Оно не удаляет секрет из других мест, куда он был скопирован: логов, тикетов, сообщений в чатах или собранных артефактов.

Вот почему важен порядок. Сначала ротация, потом уборка. Переписанная история с действующим ключом — ложное чувство безопасности. После push прогоните сканер по свежему клону и убедитесь, что секрета действительно больше нет ни в одной ветке и ни в одном теге.

Шаг 4: предотвратить следующую утечку

Цель — сделать так, чтобы закоммитить секрет было трудно, а заметить — быстро. Ни одна мера не решает обе задачи, поэтому комбинируйте их.

Держите секреты вне кода

Читайте учётные данные из переменных окружения или менеджера секретов во время выполнения и проверяйте при запуске, что все нужные на месте. Закоммитьте .env.example с значениями-заглушками и добавьте .env в .gitignore с первого коммита. Для продакшена менеджер секретов — AWS Secrets Manager, Google Secret Manager, HashiCorp Vault или зашифрованные настройки окружения вашей платформы — даёт контроль доступа, журналы аудита и упрощает ротацию.

Блокируйте секреты до коммита

Хук pre-commit ловит ошибку на машине разработчика, ещё до того, как она попадёт на удалённый сервер. Фреймворк pre-commit сводит это к нескольким строкам конфигурации, которые может установить каждый участник.

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: vX.Y.Z  # закрепите на теге последнего релиза
    hooks:
      - id: gitleaks

# Установить один раз в каждом клоне
#   pip install pre-commit
#   pre-commit install

Хуки локальны, и их можно пропустить, поэтому в качестве страховки запускайте тот же сканер в CI при каждом push. На GitHub включите secret scanning и push protection, если ваш тариф их поддерживает; push protection на стороне сервера отклоняет push, содержащие распознанные токены провайдеров.

Уменьшайте ущерб от пропущенных утечек

  • Ограничивайте каждый ключ минимальными правами и, если провайдер позволяет, конкретными IP-адресами или источниками запросов.
  • Предпочитайте краткоживущие учётные данные, например федерацию OIDC из CI в облачного провайдера, долгоживущим статическим ключам.
  • Используйте отдельные ключи для каждого окружения, чтобы утёкший тестовый ключ не мог затронуть продакшен.
  • Держите регламент ротации для каждого критичного секрета, чтобы ротация под давлением была рутиной, а не импровизацией.

Место ревью кода

Сканеры хорошо распознают известные форматы, но слабее понимают контекст — например, пароль, собранный из двух констант, или учётные данные по умолчанию, зашитые в резервный вариант конфигурации. Такое ловит ревью кода. CodeAuditAgent помечает жёстко прописанные учётные данные как CWE-798 с цитатой-доказательством при аудите ветки по умолчанию публичного репозитория или вставленного фрагмента. Он читает текущий код, а не историю коммитов, поэтому для прошлых коммитов держите отдельный сканер истории.

Короче говоря: закоммиченный секрет — это утёкший секрет. Замените его сегодня, очистите историю, если это стоит неудобств, и сделайте так, чтобы следующая утечка останавливалась на хуке pre-commit, а не оказывалась на публичной странице.