- Безопасность
- 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-verifiedgitleaks и 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, а не оказывалась на публичной странице.