Глубокая зачистка секретов в Gitlab
Итак, мы случайно git commit -a -m "I don't know how to use git".
Ну, или просто проморгали тот факт, что после сеанса неистового дебага в коммите оказались креды от какого-нибудь особо важного сервиса, которые там быть не должны.
Все мы люди, в конце концов.
Поэтому сейчас будем разбираться как сделать так, чтобы минимизировать ущерб.
Сценарий первый: ничего не предвещает
Мы заметили вовремя, что что-то пошло не так и не сделали git push.
Вешаем себе медальку за внимательность, редактируем файл, делаем git commit --amend и радуемся жизни.
Но это только если оплошность произошла в последнем коммите.
Если оплошность произошла несколько коммитов назад и мы так и не сделали git push, то тут есть два плюс-минус нормальных варианта.
git reset --soft <commit_sha>, редактируем файл и снова создаём чистый коммит.git rebase -i <commit_sha>и наслаждаемся увлекательным процессом интерактивного ребейза.
Ну и ещё альтернативный рут с использованием фиксапов, но это изначально надо в рабочем пайплайне иметь.
Сценарий второй: Почему я идиот?
Мы не заметили вовремя, что что-то пошло не так и всё же сделали git push.
Во-первых — любые попавшие в паблик креды нужно максимально быстро ревокнуть.
Во-вторых — готовимся, что нас будут бить. Возможно даже ногами.
Чтобы били не так сильно, делаем всё то же, что и в первом случае, а потом git push --force-with-lease
И обычно на этом этапе заканчивается большинство гайдов "Что делать если я запушил креды в гит?", а также по-настоящему начинается то, чем я хочу поделиться.
Прошаренные люди знают, что в Git существует reflog, и переписанный коммит всё равно остаётся на диске доступный для восстановления.
И люди, которые делают Gitlab тоже об этом знают, поэтому Гитлаб периодически запускает housekeeping таски, подчищающие таки висящие коммиты.
Чтобы не ждать, пока уборщица подметёт за вами, почистить мусор можно в Settings > General > Advanced > Prune unreachable objects.
Однако есть случаи, когда эта кнопка, несмотря на свой опасный красный цвет, не зачищает висящий коммит.
Проверить этом можно, попытавшись открыть его прямой ссылкой. Что-то вроде gitlab.example.com/group/project/-/commit/46a9d30f7a7d86adf34299367b0be7ffd0700eb60b079f3cad246c57409bd471
Показывает 404? Всё хорошо (относительно). А иногда снова показывает старый код, и вот они наши креды прям открытм текстом.
Сценарий третий: Господи, за что мне это?
Тут следует сделать отступление и поговорить про вещи, которых нет в гите, но есть в Гитлабе. CI/CD и Merge Requests.
Мы же как-то можем видеть в окне MR'а список коммитов, диффы и вот это вот всё.
И оно никуда не исчезает. Даже когда ветка удалена, MR закрыт и про это уже все забыли.
Особенно приятно это узнать, когда репозиторий существует несколько лет и кто-то когда-то оставил ключ от API в отдельной ветке, в одном из самых первых MR.
Бонусные очки, если репозиторий достался в наследство и доступы к аккаунту уже пять лет как потеряны, а на носу SOC 2 аудит.
И именно об этом сценарии мы сейчас поговорим. Именно в контексте селфхостед Гитлаба, потому что на облаке нам рута никто не даст.
Также небольшой дисклеймер, что написанное относится к Gitlab 18.6.2, а весь механизм keep-around'ов нервирует не одну меня, так что в будущем всё может измениться.
Зачистка основной истории
СДЕЛАЙ БЭКАП!!!
Вот прям капсом и с тремя восклицательными знаками. Продолбать нужные данные при переписывании истории — это неиллюзорный шанс и лучше бы иметь точку восстановления.
Дальше нам надо запустить произвольно выбранный сканер и записать полные хеши коммитов, в которых оказались креды.
Ну, просто потому что это удобно будет, да и сканер по десять раз гонять — процесс долгий.
А вот дальше уже можно запускать BFG, git-filter-repo или ещё какую тулзу для зачистки истории от нежелательных строк. Тут вообще не принципиально.
Дальше git push --force --all, вот это вот всё, ну там в мануале тулзы будет написано.
Поиск сущностей
Прежде чем начать зачищать удалённый репозиторий, нужно бы отследить где вообще эти коммиты используются, чтобы случайно не сломать Гитлабу интерфейс.
Так что делаем gitlab-rails console и начинаем вспоминать Ruby
commit_sha = "<target_commit_hash>"
# Список ID пайплайнов
Ci::Pipeline.where(sha: commit_sha).map(&:id)
# Список MR
MergeRequestDiffCommit.where(sha: commit_sha).map(&:merge_request_diff).map(&:id).uniq
Печатает некрасиво, зато дебажить удобно.
Я не уверена, что эти сущности обязательно удалять надо, но от греха подальше в целом можно. А то мало ли ещё где всплывёт.
Это можно и через консоль сделать, но на этом этапе паранойя обычно достигает такого уровня, что лучше уж через UI.
Поиск репозитория
Тут прежде чем открывать консоль, нужно сходить в UI и посмотреть ID репозитория. Он находится в Settings > General > Naming, descriptions, topics > Project ID
А дальше снова возвращаемся в консоль и делаем
Project.find(PROJECT_ID).repository.disk_path
Это выдаст нам относительный путь в духе @hashed/5d/0d/5d0db47fcc1b04bcf18b2f6d7d2f8d9c40589f3bf329b55baf7084cb553e8da8.git.
Относительный он к пути хранилища, так что если у вас NixOS, то это по-умолчанию /var/gitlab/state/repositories/. В случае других вариантов рекомендую свериться с чертежом.
Склеиваем вместе и поехали:
cd /var/gitlab/state/repositories/@hashed/5d/0d/5d0db47fcc1b04bcf18b2f6d7d2f8d9c40589f3bf329b55baf7084cb553e8da8.git
# Ветки, в которых остался коммит
git branch --contains <commit_sha>
# Теги, в которых остался коммит
git tag --contains <commit_sha>
# Рефы, в которых остался коммит
git for-each-ref --contains <commit_sha>
Поиск в ветках и тегах обязательно должен выдавать пустой результат. Если результат не пустой — что-то недочистили и время вернуться к первому шагу.
А вот список рефов из последней команды — это те самые штуки, которые не дают Гитлабу нормально зачистить коммиты. Выглядит это как-то так:
d6cb0ea8746201dfb0a9303563bd52e31555541a25fc9e1dbfda8f757a9c92bb commit refs/keep-around/d6cb0ea8746201dfb0a9303563bd52e31555541a25fc9e1dbfda8f757a9c92bb
4ef7ba090eb92e6a95f727b73cf2f1979ba83ee6bb63a4dc902790793b9274c6 commit refs/keep-around/4ef7ba090eb92e6a95f727b73cf2f1979ba83ee6bb63a4dc902790793b9274c6
fd3fa98ec1d67c155f2e9900023460bd4c41f6b87b2344188a4a1b1b51ee6fbf commit refs/keep-arount/fd3fa98ec1d67c155f2e9900023460bd4c41f6b87b2344188a4a1b1b51ee6fbf
d602c43e76a1e51e77b9aa441f5250d14b05ce55b213214aa287610b22b8df5a commit refs/merge-requests/512/merge
ae2e6f410385c1e78c2be5bb839299b14db296a9048ddb04a5605e035e023816 commit refs/merge-requests/712/merge
Логично предположить, что refs/merge-requests относятся к merge request'ам, а вот с refs/keep-around чуточку другая история. Насколько я поняла (а поняла я не очень хорошо) Gitlab создаёт эти рефы для CI тасок, но вроде и не только. Поэтому их зачистка может сломать что-то кроме CI, который мы (в теории) зачистили на прошлом этапе.
Так что на свой страх и риск:
git update-ref -d <ref_name>
И в конце с чистой совестью делаем
git reflog expire --expire=now --all
git gc --aggressive --prune=now
Сценарий четвёртый: Hans, get the flammenwerfer!
git@sweetiebelle in /var/gitlab/state/repositories/52/66/5266a2866c3d458909112a2a85958e690e5b5b10ac3ef8e97b50f195ff145d74.git λ git for-each-ref --contains b682940 | wc -l
6251
ДА БЛЯ!!!
Пойду репозиторий заново создавать.