Секрет попадает в репозиторий почти всегда одинаково. Разработчик случайно коммитит файл с ключом доступа, замечает это, удаляет файл, делает коммит «убрал лишнее» и выдыхает.
Ключ при этом остаётся на месте: он лежит в объектах истории и достаётся одной командой. У всех, кто успел склонировать, — тоже.
Как ключ попадает в историю
Собираю ситуацию с нуля:
git init -q echo 'API_KEY=sk-test-0000000000000000' > config.env git add . && git commit -qm 'add config' rm config.env git add -A && git commit -qm 'remove secret config'
История выглядит убедительно:
3b41bf3 remove secret config 1b1f9cd add config
Файла в рабочем каталоге нет, в последнем коммите его тоже нет. Теперь ищем:
git log -p --all | grep -c 'sk-test-0000000000000000'
2
Два вхождения: добавление и удаление. Точнее — через поиск по всем коммитам:
git grep 'sk-test' $(git rev-list --all)
1b1f9cd365abbd8bb5426ab8e5ab4aa9a986a8a4:config.env:API_KEY=sk-test-0000000000000000
Git устроен так, что каждая версия каждого файла хранится как отдельный объект. Коммит, который «удаляет» файл, — это просто новое состояние дерева, где файла нет. Старый объект с содержимым остаётся в базе и доступен по хешу.
Найти такие объекты можно и не зная, где искать:
git rev-list --objects --all | head
Почему git rm и переписывание истории помогают не всегда
Допустим, вы знаете правильный ответ: переписать историю через git filter-repo или BFG, выкинув файл из всех коммитов. Это работает для вашего репозитория. Дальше начинаются нюансы, о которых обычно не думают:
У всех, кто уже склонировал, история прежняя. Переписывание у вас не удаляет объекты у них. Каждая копия репозитория — полноценная база со всеми версиями.
На хостинге объекты живут после переписывания. Ссылки на старые коммиты остаются доступными по прямому адресу, пока сборщик мусора их не уберёт, а сроки тут не гарантированы. Плюс в форках объекты остаются независимо от того, что вы сделали у себя.
Автоматика уже увидела. Публичный репозиторий сканируется ботами непрерывно — счёт идёт на минуты. Пока вы переписываете историю, ключ уже проверен и, если рабочий, использован.
Отсюда правило: любой секрет, попавший в коммит, считается скомпрометированным. Не «возможно скомпрометированным» — просто скомпрометированным.
Правильный порядок действий
Сначала — отозвать и перевыпустить ключ. Это первое, а не последнее. Пока ключ действует, всё остальное не имеет смысла.
Потом — проверить, где он использовался: логи доступа провайдера, счета за услуги, необычная активность. Отозванный ключ мог поработать несколько часов.
И только затем — чистить историю: git filter-repo, форс-пуш, уведомление всем, кто работает с репозиторием, чтобы переклонировали.
Как не попадать
Проверка перед коммитом. Хук pre-commit с поиском по шаблонам ключей: gitleaks, detect-secrets, trufflehog. Ловят не всё, но типовые ключи с узнаваемым префиксом закрывают, а настройка сводится к одному файлу конфигурации.
Проверка на стороне сервера. Хостинги умеют сканировать пуши и блокировать те, где нашлись секреты. Включается галочкой в настройках репозитория.
.gitignore с самого начала проекта. *.env, *.pem, credentials.json, каталоги с конфигурацией окружений. Дописывают его обычно уже после первого инцидента.
Секреты не в файлах, а в хранилище. Переменные окружения из менеджера секретов, а не config.env рядом с кодом. Тогда в репозитории нечему утекать.
И полезная привычка на будущее: прежде чем считать, что старый секрет из репозитория убран, выполните git grep по всем коммитам. Результат иногда неожиданный — особенно в репозиториях, которым несколько лет.

