Случалось ли вам добавлять в Git что-нибудь по ошибке? Если это было что-то вроде функции для дебага, некорректного комментария или случайно скопированного файла, то можно это исправить следующим коммитом. Но сработает ли такое исправление, если вы (или ваш коллега) случайно добавили в репозиторий какой-нибудь секрет? Ключ от стороннего сервиса, логин/пароль от интеграции или даже дамп базы данных или всю папку /private/uploads? На первый взгляд - да, сработает, можно сделать новый коммит и удалить то, что было добавлено по ошибке. Но дело в том, что эти данные все равно останутся в репозитории. По истории коммитов можно вернуться назад во времени (до исправления) и увидеть данные, которых уже нет в последней версии репозитория.

Обычно в репозиторий (даже в приватный) принято не добавлять никакие чувствительные данные. Обычно в контракте с заказчиком это будет прописано отдельной строкой. Причины очевидны: процесс выдачи паролей или ключей интеграций должен иметь возможность отследить, кому они были выданы, при необходимости отозвать или изменить их. Даже в приватном репозитории доступ к этим секретам получит любой, кто может читать репозиторий, или его клон, форк. А также ваши CI/CD и прочие интеграции с репозиторием. И потом доступ на чтение репозитория не должен автоматически означать доступ к реальной базе данных, интеграциям, или личным данным кого-либо. Надеюсь, я убедил, что никаких “секретов”, дампов базы и чувствительных данных в репозитории быть не должно, даже если их добавили не вы, это случилось давно или вы даже не в курсе. Я имею в виду в репозитории вообще, а не только в его последнем состоянии.

Если в репозиторий попал действующий API-ключ, пароль или токен, хорошей практикой считается в первую очередь отозвать его или заменить новым.

Давайте проверим любой ваш репозиторий на предмет присутствия в нем чувствительных данных! Например, вы присоединились к проекту, который активно разрабатывается уже пару лет. Кто вообще знает, что туда добавляли? А мы сейчас посмотрим и, если нужно, исправим.

Можно, конечно, составить промпт для ИИ и поручить ей “очисти историю коммитов от паролей и прочих чувствительных данных, но нужное не удаляй и переспрашивай если надо” и, вероятно, она справится. Можно даже на основе того, что написано ниже создать агента и научить его пользоваться этими утилитами. Можно даже добавить подобную проверку в процесс CI/CD. Но можно и вручную. План такой:

1. Проверить, что в репозитории лишнее

Для изучения репозитория Git есть много разных утилит. Могу порекомендовать https://github.com/betterleaks/betterleaks. Раньше я использовал https://github.com/gitleaks/gitleaks но все быстро менятся, утилиты устаревают, появляются новые… Надо не отставать от жизни.

В основе работы большинства подобных сканеров лежат правила обнаружения, по ключевым словам и регулярным выражениям. Например, правило может искать конструкции, похожие на AWS ключ, GitHub токен и тому подобное. Как один из этапов, утилита Betterleaks использует быстрый фильтр по ключевым словам чтобы найти что то вроде “api_key:123abc”, а затем использует BPE-токенизацию (Byte Pair Encoding) - тот же общий подход к токенизации, который используется в современных языковых моделях. Обычный человеческий текст хорошо разбивается на знакомые длинные токены, а случайная строка вроде API-ключа - на множество маленьких фрагментов. Это дополнительный механизм, который помогает снижать количество ложных срабатываний. Кроме того, умеет автоматически декодировать, например, Base64, hex и URL encoding, а затем проверять результат, а также проверять секреты внутри архивов.

Установка

(Далее взято из https://medium.com/devsecops-ai/i-switched-from-gitleaks-to-betterleaks-heres-what-changed-de286168bf9c)

# Homebrew — easiest on macOS or Linux
brew install betterleaks
# Docker — useful for ephemeral CI environment
docker pull ghcr.io/betterleaks/betterleaks:latest
# Build from source
git clone https://github.com/betterleaks/betterleaks cd betterleaks make betterleaks

Проверка, что установилось и работает

betterleaks version

Проверка репозитория Git

Перейдите в консоли в каталог проекта и запустите

betterleaks git . --log-opts=“–all” -v

Betterleaks просмотрит каждый коммит, каждый патч и каждое изменение. Если ключи или пароли появляются где-либо в истории репозитория, они появятся в отчете как подозрительные - даже если файл был удален десять коммитов назад.

В результате увидите что-то вроде:

[ 
  { 
    “RuleID”: “generic-api-key”, 
   “Description”: “Detected a Generic API Key, potentially exposing access to various services and sensitive operations.”, 
   “StartLine”: 4, 
   “EndLine”: 4, 
   “StartColumn”: 6, 
   “EndColumn”: 50, 
   “Match”: “key: cTAAAAARqyHh71UM**MXjiZKhI", 
   “Secret”: "6LAAV****UMIEGNQKhI", 
   “Attributes”: 
   { 
   “confidence”: “low”, 
   “git.author_email”: ".@.”, 
   “git.author_name”: “***** ", 
   “git.date”: "20-0-0*T07::43Z”, 
   “git.message”: “-9 Webforms switched to v3 reCaptcha.", 
   “git.platform”: “bitbucket”, “git.remote_url”: "https://bitbucket.org/", 
   “git.sha”: "************", 
   “path”: “config/sync/recaptcha_v3.settings.yml”, 
   “resource”: “git.patch_content”, 
   “url”: "https://bitbucket.org///config/sync/recaptcha\_v3.settings.yml#lines-4" 
  }, 
  “Tags”: [], 
  “Fingerprint”: "07016e86988ce525f70d2:config/sync/recaptcha_v3.settings.yml:generic-api-key:4", 
  “File”: “config/sync/recaptcha_v3.settings.yml”, 
  “SymlinkFile”: “”, 
  “Commit”: "********”, 
  “Entropy”: 4.6153116, 
  “Author”: “***** ", 
  “Email”: " @***’, 
  “Date”: "20-0-T07::43Z", 
  “Message”: " Webforms switched to v3 reCaptcha.” 
  },
]

Видно, кто, куда и когда закоммитил лишнее.

Но можно настроить вывод результатов в файл.

betterleaks git . --log-opts=“–all” --report-format json --report-path …/betterleaks-report.json -v

Параметр --log-opts=“–all” говорит Betterleaks проверять все ветки, а не только текущую. Смотрите --help или описание самой утилиты.

Но не стоит считать что все, что нашлось, нуждается в удалении: там могут быть ложные срабатывания. Нужно вручную проверить найденные значения и решить, действительно ли они являются секретами.

Можем собрать статистику в комментариях, у кого что нашлось :)

2. Удалить лишнее из репозитория

Результат сканирования будем использовать другой утилитой, которая умеет удалять нужные части строк из всех коммитов репозитория. Это https://github.com/newren/git-filter-repo Для нее, на основе найденых результатов, надо составить файл замены. Можно сделать вручную, а можно конвертировать полученный выше файл (команды составлены ИИ):

Собрать секреты из отчета в файл замен для filter-repo

jq -r ‘.[].Secret’ …/betterleaks-report.json | sort -u > …/secrets-raw.txt

Затем надо превратить в формат filter-repo: значение==>замена: Ниже - пример конвертации JSON-отчёта в формат git-filter-repo; конкретную команду стоит проверить на версии вашей Betterleaks и структуре полученного отчета.

awk ‘{print $0"==>REMOVED"}’ …/secrets-raw.txt > …/secrets-to-replace.txt

Теперь найдем “опасные” файлы просто на основе их расширений (команда составлена ИИ):

git log --all --diff-filter=A --name-only --pretty=format: – 
 ‘.sql’ '.sql.gz’ ‘.dump’ '.bak’ ‘*.env’ | sort -u > …/paths-to-remove.txt

Адаптируйте команду изменив список расширений файлов которые она ищет.

Сейчас у нас появились два файла: со списком файлов на удаление и со списком замен.

Следующим шагом - начнем очистку! 🙂 (начали уже смотреть 3-й сезон Silo?)

Установка утилиты https://github.com/newren/git-filter-repo :

brew install git-filter-repo

Затем надо вручную перенести имеющиеся в репозитории секреты в конфигурацию вашего приложения, которая не является частью репозитория. Это зависит от проекта и технологии которую вы используете, используйте имеющуюся возможность переназначать значения конфигурации через settings.local.php, .env.local и тому подобное. То есть надо чтобы ваш прод работал со значениями из другого источника. Git-filter-repo чистит историю, и так же затрагивает последний коммит. Если вы проигнорируете предыдущий шаг, это может сломать работу приложения при деплое и вы можете даже потерять свои ключи, если они были только в репозитории, после того, как они удалятся.

Дальнейшие действия деструктивны и необратимы, ведут к удалению данных, будьте осторожны, согласуйте это с командой, менеджером проекта и заказчиком. На время работы никто не должен делать коммиты.

Обязательно делаем резервную копию

git clone --mirror  repo-backup.git

Держите эту копию отдельно пока не убедитесь, что все прошло хорошо. И еще пару недель после этого 🙂 Не используйте эту копию как рабочую и не делайте в неё push очищенной истории.

Закройте все имеющиеся Pull/Merge requests - история и хеши коммитов будут переписаны. PR придется открывать заново после.

Далее сделаем еще одну копию в которой и будем делать очистку

git clone  repo-clean cd repo-clean

git-filter-repo требует “чистый” клон (утилита убирает remote origin после работы - защита от случайного push)

Далее сама очистка

Один проход git-filter-repo на оба действия файлы + замена текста секретов

git filter-repo 
 –invert-paths --paths-from-file …/paths-to-remove.txt 
 –replace-text …/secrets-to-replace.txt

Проверка результата

git log --all --oneline | wc -l

история не развалилась

Повторный скан — findings должны исчезнуть

betterleaks git . --log-opts=“–all” -v

Локальная зачистка старых объектов и передача изменений на сервер

git-filter-repo выполняет очистку и repack самостоятельно. При необходимости можно дополнительно выполнить более агрессивную очистку локального репозитория:

git reflog expire --expire=now --all git gc --prune=now --aggressive

Force-push переписанной истории

Добавляем remote репозиторий обратно

git remote add origin 

Внимание: переписываем историю на сервере

git push --force --mirror origin

И последнее: после переписывания истории, самый безопасный вариант для ваших коллег - удалить старую рабочую копию и заново клонировать репозиторий именно не pull/rebase, а клонировать репозиторий заново т.к. хэши коммитов стали другие. И CI/CD деплой-скрипты, которые работают с Git, тоже должны получить свежий клон, а не инкрементальный fetch.

Для полноты картины надо сказать, даже после force-push нельзя автоматически считать, что старые данные исчезли абсолютно отовсюду. Они могли остаться в старых клонах, форках, резервных копиях, CI/CD-кэшах, артефактах, логах и т.п. Кроме того, конкретный Git-хостинг может некоторое время сохранять старые объекты или cached views. Поэтому кроме удаления из Git-истории утекшие ключи необходимо отозвать и заменить на новые, то есть сделать их ротацию.