Токен уехал в публичный репозиторий: что отзывать и в каком порядке
Коммит с .env файлом или тестовым конфигом улетел в публичный репозиторий на GitHub — а через несколько минут кто-то замечает, что там был живой API-ключ или пароль от продовой базы. Первый инстинкт — тихо удалить файл новым коммитом и понадеяться, что никто не заметил. Это не работает: секрет уже в истории git, а публичные репозитории сканируются ботами практически в реальном времени. Разберём правильный порядок действий — что отзывать в первую очередь, зачем чистить историю отдельным шагом и как оценить, успел ли кто-то воспользоваться утечкой.
Содержание
Почему «удалить коммит и запушить» не решает проблему
Интуитивно кажется логичным: увидел лишний файл — удалил, закоммитил, запушил. Проблема в том, что git не забывает. Новый коммит просто добавляет запись «файл удалён» поверх истории — сам секрет остаётся во всех предыдущих коммитах, доступных через git log, git show, обычный клон репозитория или даже просмотр через веб-интерфейс GitHub по прямой ссылке на старый коммит.
Проверить это можно за секунды:
git log --all --full-history -- .env
git show <commit-hash>:.env
Если репозиторий был публичным хотя бы недолго, любой, кто успел его клонировать, форкнуть или просто открыть в кэше поисковика, унёс секрет с собой — и последующая история коммитов на это уже никак не влияет. GitHub Pages, зеркала, кэш raw.githubusercontent.com — секрет мог разойтись по нескольким копиям, каждая из которых живёт своей жизнью.
Второй и более острый момент — скорость реакции автоматики. Крупные площадки (GitHub, GitLab) гоняют собственные сканеры секретов (secret scanning) по каждому публичному пушу, а независимо от них по интернету постоянно ходят боты сторонних злоумышленников, которые мониторят публичные репозитории на предмет паттернов, похожих на ключи AWS, токены Stripe, строки подключения к базам данных и так далее. Речь идёт не о днях и не о часах — совпадения находят счёт на минуты, иногда секунды после пуша. Поэтому к моменту, когда вы вспомнили удалить файл, секрет вполне мог быть уже собран и добавлен в чей-то список для проверки на валидность.
Отсюда главный вывод: чистка истории git — это гигиена, а не защита. Она не отменяет того, что секрет уже был публично виден. Защита — это отзыв самого секрета в сервисе, который его выдал.
Порядок действий: сначала отзыв, потом уборка
Правильная последовательность действий при обнаружении утечки секрета в git-репозитории:
- Немедленно отозвать или перевыпустить скомпрометированный секрет в сервисе, который его выдал — это единственное действие, которое реально закрывает риск.
- Проверить логи использования секрета на аномальную активность за период, пока он был активен и виден.
- Только после отзыва заняться очисткой git-истории —
git filter-repoили BFG Repo-Cleaner. - Force-push очищенной истории и уведомление всех, кто клонировал репозиторий, о необходимости пересоздать локальные копии.
- Разбор причины — почему секрет вообще оказался в репозитории, и настройка защиты от повтора.
Ключевая ошибка большинства инструкций в интернете — они начинают с пункта 3, потому что технически это интереснее. На практике пункт 1 должен случиться в первые минуты, до того как вы вообще открыли документацию по git filter-repo. Пока секрет действителен, каждая минута — это окно для злоумышленника, независимо от того, что происходит с git-историей.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверШаг 1: отзыв секрета в сервисе-источнике
Что именно делать, зависит от типа секрета, но логика одна — обесценить утёкшие данные так, чтобы владение ими больше ничего не давало.
API-токены облачных провайдеров и SaaS. Идёте в панель управления сервиса (AWS IAM, Stripe, SendGrid, Telegram Bot API и т.д.) и отзываете конкретный ключ — не «на всякий случай меняете пароль от аккаунта», а именно инвалидируете скомпрометированный токен. У большинства провайдеров это отдельная кнопка «Revoke» или «Delete key» в разделе API-ключей. После отзыва старый токен перестаёт проходить аутентификацию мгновенно или в течение нескольких минут — уточняйте у конкретного сервиса, у некоторых есть задержка кэша.
Пароли баз данных. Меняете пароль пользователя СУБД и обновляете строку подключения во всех местах, где она реально используется (приложение, бэкап-скрипты, ORM-миграции, дашборды мониторинга):
-- PostgreSQL
ALTER USER app_user WITH PASSWORD 'новый-длинный-случайный-пароль';
-- MySQL
ALTER USER 'app_user'@'%' IDENTIFIED BY 'новый-длинный-случайный-пароль';
FLUSH PRIVILEGES;
Если база слушает не только localhost, а доступна извне — параллельно стоит проверить, что доступ и так ограничен файрволом или security group, а не только паролем.
SSH-ключи. Если в репозиторий попал приватный ключ (частый случай — id_rsa случайно закоммичен вместе с конфигом деплоя), удаляете соответствующий публичный ключ из ~/.ssh/authorized_keys на всех серверах, где он был разрешён, и генерируете новую пару:
ssh-keygen -t ed25519 -C "deploy-key-new" -f ~/.ssh/deploy_new
Если тем же ключом пользуются несколько серверов или CI-раннер — обновить нужно везде одновременно, иначе часть инфраструктуры продолжит доверять скомпрометированному ключу.
JWT-секреты и подписи сессий. Это отдельный случай: смена JWT_SECRET мгновенно делает недействительными вообще все выданные токены, включая живые сессии легитимных пользователей. Логика та же — отозвать, — но здесь стоит заранее продумать, как аккуратно разлогинить всех и не устроить лавину тикетов в поддержку в неудачный момент.
Шаг 2: оценка — успели ли секретом воспользоваться
Отозвать секрет — не значит закрыть вопрос. Нужно понять, был ли он использован за то время, пока оставался действительным и публично виден. Это расследование, а не формальность.
Что смотреть в первую очередь:
- Логи доступа сервиса, выдавшего токен. У большинства облачных API есть журнал вызовов (CloudTrail у AWS, audit log у Stripe и аналогов) — ищите запросы с IP-адресов, которые не принадлежат вашей инфраструктуре, или необычную активность (массовые запросы, обращения к ресурсам, которые вы не трогали, попытки с географии, которой у вас нет).
- Время публикации коммита и время обнаружения. Чем больше разрыв между пушем в публичный репозиторий и отзывом секрета, тем выше вероятность, что кто-то успел его подобрать. Разрыв в 5 минут — риск невелик, но не нулевой (боты сканируют быстро). Разрыв в несколько дней или недель — считайте, что секрет скомпрометирован по умолчанию.
- Логи самого сервиса, использующего секрет. Если это ключ к базе данных — смотрите на аномальные подключения, новые запросы
SELECTна таблицы, к которым обычно не обращаются, попытки массовой выгрузки. Для SSH — проверьтеlast,/var/log/auth.logна предмет входов с незнакомых IP в интересующий период. - Списки известных диапазонов ботов-сканеров. Часть подозрительной активности — это автоматизированная проверка «а рабочий ли токен» без злого умысла в моменте (данные потом попадают в базы для перепродажи). Отличить безобидное сканирование от целевой атаки по одним логам сложно — поэтому при любом сомнении действуйте так, как будто секрет был использован.
Если однозначных следов компрометации не нашли — это хорошая новость, но не повод расслабляться: отсутствие доказательств использования не равно доказательству того, что использования не было. Логи могли не покрывать нужный период, злоумышленник мог действовать аккуратно, чтобы не наследить. Отозванный секрет снимает риск на будущее независимо от результата этого расследования.
Шаг 3: очистка git-истории
После того как секрет обесценен, можно спокойно, без спешки, вычистить его из истории репозитория — это уже вопрос гигиены и порядка, а не безопасности.
Два стандартных инструмента: git filter-repo (современная замена git filter-branch, официально рекомендуемая) и BFG Repo-Cleaner (проще в использовании для типовых случаев).
Через git filter-repo:
pip install git-filter-repo
# удалить конкретный файл из всей истории
git filter-repo --path .env --invert-paths
# или заменить конкретную строку/секрет во всех файлах
echo "ghp_старыйтокензначение==>УДАЛЕНО" > replacements.txt
git filter-repo --replace-text replacements.txt
Через BFG (нужна Java):
java -jar bfg.jar --delete-files .env repo.git
# или замена конкретных строк
java -jar bfg.jar --replace-text passwords.txt repo.git
После любого из вариантов история переписана локально — дальше нужно принудительно обновить удалённый репозиторий и почистить рефлог и мусор:
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push origin --force --all
git push origin --force --tags
Важный нюанс: force-push переписывает историю для всех. Если репозиторием пользуется команда, каждому нужно сообщить, что нужно не мержить старую локальную копию, а полностью пересоздать её (git clone заново или git reset --hard на новый origin) — иначе кто-то случайно вольёт старую историю с секретом обратно очередным пушем, и вся работа насмарку.
Отдельно стоит зайти в настройки самого GitHub/GitLab и проверить кэш: у GitHub есть механизм очистки кэшированных представлений старых коммитов и форков по запросу через поддержку — сам force-push автоматически не чистит их со стороны платформы.
Как не допустить повтора
Разовая уборка не защищает от следующего случайного коммита — нужна системная защита на уровне процесса.
Pre-commit хуки со сканированием секретов. Инструменты вроде gitleaks или detect-secrets можно повесить локально перед каждым коммитом:
# .git/hooks/pre-commit (или через pre-commit framework)
gitleaks protect --staged --verbose
Это ловит секрет до того, как он вообще попадёт в git, а не после.
.gitignore для конфигов с самого начала проекта, а не когда уже поздно:
.env
.env.*
*.pem
*.key
config/secrets.yml
Секреты — не в коде, а в переменных окружения или менеджере секретов (Vault, Docker secrets, переменные окружения CI/CD). Файл .env.example с плейсхолдерами вместо реальных значений — коммитится, реальный .env — никогда.
Server-side сканирование в CI/CD как второй рубеж на случай, если pre-commit хук кто-то обошёл (git commit --no-verify) — GitHub Advanced Security, GitLab secret detection или тот же gitleaks как отдельный шаг в пайплайне, который блокирует мердж при обнаружении паттерна секрета.
Про то, как вообще организовать хранение паролей и переменных окружения в контейнерных проектах, чтобы такие утечки не случались в принципе, есть отдельный разбор — управление секретами в Docker. А если утечка секрета сопровождалась ещё и падением сервиса из-за протухшего токена — это уже смежный, но другой сценарий, разобранный в статье про просроченный токен и сломанное автообновление.
Что делать, если репозиторий приватный, но всё равно тревожно
Отдельная ситуация — секрет попал не в публичный, а в приватный репозиторий, но всё равно вызывает беспокойство: например, к репозиторию имеет доступ внешний подрядчик, или репозиторий позже случайно стал публичным на какое-то время, или используется общий GitHub Actions runner.
Логика та же самая, но с поправкой на масштаб: если утечка ограничена кругом людей с легитимным доступом к репозиторию — риск ниже, но не нулевой (скомпрометированный аккаунт одного из участников команды даёт доступ ко всей истории). Если репозиторий хотя бы недолго был публичным (например, ошибочно переключили видимость и вернули обратно через несколько минут) — считайте секрет скомпрометированным по умолчанию, GitHub успевает проиндексировать содержимое быстро, а обратное переключение в приватный режим уже ничего не убирает из внешних копий и кэшей.
Проверить историю видимости репозитория можно в настройках Settings → Danger Zone → change repository visibility — там виден текущий статус, но история переключений в бесплатных тарифах не всегда доступна в явном виде, так что при малейшем сомнении лучше уточнить у всех участников команды, не клонировал ли кто-то репозиторий в момент, когда он был открыт.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Секрет был в приватном форке или в закрытом Pull Request — тоже нужно отзывать?
Да. Приватность форка не отменяет того, что секрет технически существовал в git-объектах, доступных всем, у кого есть доступ к этому форку или PR, включая CI-раннеры, которые его собирали и могли залогировать значение в вывод сборки.
Можно ли обойтись без force-push и просто оставить секрет в старой истории после отзыва?
Технически можно — если секрет отозван, само его наличие в старых коммитах уже не даёт доступа ни к чему. Но это плохая практика: старый ключ продолжает выглядеть как действующий для инструментов сканирования, создаёт шум и путаницу, и если случайно «отзыв» окажется неполным (например, забыли один из нескольких активных ключей у сервиса) — риск остаётся скрытым в истории.
GitHub сам присылает уведомление об утечке — стоит ли на это полагаться?
Secret scanning от GitHub покрывает известные форматы токенов популярных провайдеров (AWS, Stripe, многие облачные API) и уведомляет как вас, так и иногда сам провайдер токена, который может отозвать его автоматически. Но это не покрывает произвольные пароли баз данных, кастомные API-ключи или JWT-секреты собственного изготовления — там придётся полагаться только на pre-commit хуки и внимательность.
Сколько по времени в среднем длится окно между пушем и подбором секрета ботом?
Точных общих цифр по всему интернету никто не публикует, но по наблюдениям сообщества безопасности речь идёт о минутах, иногда о секундах для распространённых форматов ключей (AWS, GitHub personal access tokens) — рассчитывайте на худший сценарий и не откладывайте отзыв.
Нужно ли менять пароль от самого GitHub/GitLab аккаунта, если утёк не пароль от него, а секрет проекта?
Само по себе не обязательно — это разные уровни доступа. Но если есть основания думать, что утечка секрета — следствие компрометации именно вашей учётной записи (а не случайного коммита), тогда да, и дополнительно стоит проверить включённую двухфакторную аутентификацию и активные сессии в настройках аккаунта.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →