MAATRIX / Блог / Захардкоженные пароли в чужом коде: как найти их все до того, как найдут другие

Захардкоженные пароли в чужом коде: как найти их все до того, как найдут другие

MAATRIX

Вы открываете репозиторий, который достался вам от прежней команды, и в третьем же файле конфига видите DB_PASSWORD = "prod_2019!" прямо в коде. Первая реакция — исправить именно эту строчку. Но если пароль от базы лежал открытым текстом в одном файле, скорее всего он не единственный: захардкоженные секреты редко бывают инцидентом, обычно это стиль работы, который практиковали годами. Дальше — методичный поиск: что грепать руками, какие инструменты находят то, что вы пропустите, как проверить историю git, куда переносить найденное и что делать с паролями, которые всё ещё действуют.

Почему один найденный пароль — повод искать дальше, а не расслабляться

Захардкоженный секрет в коде — это почти всегда симптом привычки, а не разовая ошибка. Если разработчик вписал пароль от базы прямо в config.py, велика вероятность, что тем же способом в код попали:

  • пароль от SMTP, через который уходят письма;
  • ключ платёжного шлюза или его тестовый аналог, который по ошибке стал боевым;
  • токен доступа к облачному хранилищу (S3, любому S3-совместимому объектному хранилищу);
  • пароль администратора самого приложения, зашитый как «служебный» аккаунт;
  • ключ стороннего API — карт, геокодинга, рассылок, антифрода.

Проблема усугубляется тем, что такие секреты живут не только в текущей версии файла. Их могли удалить полгода назад одной строкой правки — но история git по умолчанию хранит всё, и старый коммит с открытым паролем остаётся доступным каждому, у кого есть git clone репозитория, даже если в HEAD этой строки давно нет.

Прежде чем гнаться за инструментами, стоит принять две вещи. Во-первых, поиск никогда не даёт стопроцентной гарантии — сканеры находят то, что соответствует шаблону, а секрет, замаскированный под что-то безобидное (переменная с именем token_legacy_2), можно пропустить. Во-вторых, найти пароль — только половина работы: если секрет боевой, находка бесполезна без последующей смены значения на сервере, а не только в коде. Об этом — в конце статьи.

Что искать вручную: grep-паттерны, которые ловят большинство случаев

Перед запуском специализированных сканеров имеет смысл пройтись руками — это быстро, не требует установки ничего лишнего и даёт представление о масштабе проблемы ещё до серьёзного анализа.

Базовый поиск по ключевым словам, которые почти всегда сопровождают секрет:

grep -rniE "(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token)\s*[:=]" \
  --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=vendor .

Флаги здесь не случайны: -r — рекурсивно, -n — с номером строки (сразу видно, куда идти), -i — без учёта регистра (Password, PASSWORD, password — это всё одно и то же для человека, но не для чувствительного к регистру grep без -i), -E — расширенные регулярки, чтобы использовать группы и квантификаторы.

Отдельно стоит искать строки подключения к базам данных — они часто содержат пароль прямо внутри URL:

grep -rnE "(mysql|postgres|postgresql|mongodb|redis)://[^:\"'\s]+:[^@\"'\s]+@" .

Такой паттерн ловит вид postgres://admin:S3cret!@db-host:5432/production — классический формат connection string, где логин и пароль стоят через двоеточие перед @.

Приватные ключи и сертификаты искать проще всего по заголовку блока:

grep -rnE "-----BEGIN (RSA |OPENSSH |EC |DSA |)PRIVATE KEY-----" .

Если находка есть — это не «пароль», а закрытый ключ (SSH, TLS, подписи), утечка которого обычно серьёзнее пароля, потому что его нельзя обесценить сменой одного значения без замены самой пары ключей везде, где используется публичная часть.

Токены известных сервисов часто имеют узнаваемую сигнатуру — например, ключи AWS начинаются с AKIA или ASIA:

grep -rnE "\b(AKIA|ASIA)[0-9A-Z]{16}\b" .
grep -rnE "AIza[0-9A-Za-z_-]{35}" .   # Google API key
grep -rnE "ghp_[A-Za-z0-9]{36}"      . # GitHub personal access token

Такие сигнатурные паттерны — сила специализированных сканеров: они держат десятки подобных шаблонов под конкретные сервисы, а не только общие ключевые слова. Ручной grep неизбежно даёт много ложных срабатываний (password_field, PasswordValidator, reset_password_token без значения) и столько же пропусков (секрет в переменной с нейтральным именем KEY_2). Это нормально на этапе разведки — задача первого прохода не найти всё идеально, а понять масштаб. Если в системе уже стоит ripgrep — используйте его вместо grep, он на порядок быстрее на больших деревьях и по умолчанию уважает .gitignore: rg -i --hidden -g '!.git' '(password|secret|api_key)\s*[:=]'.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

gitleaks и trufflehog: специализированные сканеры вместо самодельных паттернов

Ручной grep хорош для первой разведки, но не масштабируется на репозиторий с тысячами файлов и многолетней историей коммитов. Для системного поиска есть два инструмента, которые остаются стандартом де-факто: gitleaks и trufflehog. Оба — open source, оба можно запустить локально без отправки кода куда-либо наружу, что критично для чужого legacy-кода с боевыми секретами.

gitleaks — сканер на Go с встроенным набором regex-правил под популярные сервисы (AWS, GCP, Stripe, Slack, GitHub и десятки других) и возможностью писать свои правила в TOML.

brew install gitleaks   # macOS
go install github.com/gitleaks/gitleaks/v8@latest   # Linux, через Go

gitleaks detect --source . --report-path gitleaks-report.json --report-format json

По умолчанию gitleaks сканирует не только рабочую копию, но и всю историю git — это его ключевое отличие от простого grep по файлам. Если нужен только текущий срез: gitleaks detect --source . --no-git.

trufflehog — конкурент с упором на верификацию: он не просто находит строку, похожую на ключ, а по возможности проверяет, действителен ли секрет прямо сейчас (делает тестовый запрос к API соответствующего сервиса).

trufflehog git file://. --only-verified        # git-репозиторий, вся история
trufflehog filesystem --directory=.             # файловая система без git

Флаг --only-verified резко сокращает шум: вместо сотен потенциальных находок вы получаете список секретов, которые trufflehog смог подтвердить как рабочие. Это удобно для приоритизации, но не заменяет полный список — неверифицированные находки тоже нужно просмотреть, потому что не все сервисы позволяют дешёвую проверку валидности без риска её испортить.

gitleakstrufflehog
Проверка действительности секретанет (только паттерн)да, для многих сервисов
Сканирование истории gitда, по умолчаниюда, режим git
Кастомные правилаTOML-конфигYAML-детекторы
Pre-commit hookgitleaks protecttrufflehog git --since-commit

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

История git хранит то, что вы давно удалили

Самая частая ошибка при поиске секретов — смотреть только в текущее состояние файлов. Если пароль был закоммичен в 2021 году и удалён коммитом в 2022-м, grep по рабочей копии его не найдёт — а он всё ещё лежит в объектах git и достаётся любому, кто клонирует репозиторий с полной историей:

# найти все коммиты, где менялся конкретный файл с секретами
git log --all --full-history -- "*.env" "config/secrets*"

# посмотреть содержимое конкретной ревизии файла
git show <commit-hash>:config/database.yml

# грубый, но рабочий способ — прогнать grep по каждому патчу истории
git log -p --all | grep -niE "password\s*="

Последняя команда честная, но медленная и шумная на репозитории с длинной историей — она выведет пароль вместе с контекстом diff для каждого коммита, где он встречался. Именно поэтому в режиме сканирования истории предпочтительнее gitleaks и trufflehog: они проходят по объектам git напрямую и выдают структурированный список находок с указанием коммита, автора и даты, а не сырой вывод diff.

Если находка подтвердилась и секрет когда-то был в истории — здесь важно разделить две разные задачи, которые часто путают:

  1. Удалить секрет из будущей истории. Это делается инструментами вроде git filter-repo или BFG Repo-Cleaner, переписывающими историю коммитов. Операция необратимо меняет хэши коммитов, требует форс-пуша и координации со всеми, у кого есть локальные клоны — сделать её на живом репозитории с несколькими разработчиками без предупреждения нельзя.
  2. Обесценить сам секрет. Сменить пароль или отозвать ключ так, чтобы то, что осталось в истории (или в чьих-то старых клонах, бэкапах, форках), больше ничего не давало.

Первое — гигиена и полезная практика, но она не отменяет второе. Если репозиторий когда-либо клонировали на внешнюю машину, отправляли подрядчику или загружали на GitHub хотя бы временно публичным — считайте, что секрет мог утечь, и перепишите историю уже после того, как обесценили сам пароль, а не вместо этого.

Что делать с каждой находкой: не всё одинаково критично

После прогона сканеров типичный результат — список из полусотни строк, часть из которых боевые пароли, часть — тестовые заглушки, а часть вообще ложные срабатывания (строка password = input() в форме логина, которую grep принял за секрет). Разбирать список эффективно помогает простая сортировка по трём вопросам для каждой находки.

Секрет вообще настоящий? Отфильтруйте явные пустышки: плейсхолдеры (password = "changeme", "your_password_here"), тестовые значения из документации фреймворка, фиктивные данные из юнит-тестов. trufflehog с --only-verified частично решает эту задачу для сервисов, которые можно опросить, но для внутренних паролей (своя база, свой сервис) верификация не сработает — такие проверяйте вручную, не логируя сам пароль в процессе.

Секрет всё ещё действует? Если код давно не деплоился с этой версией конфига, а пароль от базы менялся с тех пор по другим причинам — находка исторически ценна, но непосредственной угрозы не несёт. Если это тот самый пароль, что стоит на проде прямо сейчас, — это приоритет номер один.

Какой у секрета радиус поражения? Пароль от read-only реплики аналитической базы — не то же самое, что root-пароль от продовской СУБД или ключ, дающий доступ к платёжному провайдеру. Составьте короткую таблицу: секрет → к чему даёт доступ → критичность → кто отвечает за смену. На унаследованном сервере, где документации нет и не у кого спросить, эта таблица становится вашей единственной картой рисков — держите её отдельно от репозитория, не коммитьте.

Практический ориентир по приоритету: сначала закрывайте то, что даёт доступ к деньгам и персональным данным (платёжные шлюзы, боевые базы с клиентскими данными), затем — то, что даёт доступ к инфраструктуре (SSH-ключи, панели облака, API управления серверами), и в последнюю очередь — второстепенные интеграции с ограниченным радиусом.

Куда переносить найденное: переменные окружения и секрет-менеджеры

Найти и вырезать пароль из кода — не финал, а начало: секрет должен куда-то переехать, иначе следующий коммит вернёт его обратно тем же способом, каким он туда попал изначально.

Минимальный уровень — переменные окружения. Приложение читает значение из окружения процесса, а не из файла в репозитории:

# было
DB_PASSWORD = "prod_2019!"
# стало
import os
DB_PASSWORD = os.environ["DB_PASSWORD"]

Сами значения при этом хранятся в файле .env, который обязательно должен попасть в .gitignore до первого коммита с реальными значениями — если файл уже закоммичен, .gitignore задним числом не поможет, он остановит только будущие коммиты. Проверить, что .env не в индексе: git ls-files | grep -i '\.env$'. Если команда что-то вывела — выполните git rm --cached .env.

У переменных окружения есть свои слабые места: они видны через /proc/<pid>/environ любому с достаточными правами на той же машине, попадают в дампы при аварийном завершении процесса, иногда протекают в логи через сообщения об ошибках или debug-режим — эту тему подробнее разбирали в статье «Пароли и токены в логах: как они туда попадают и как вычистить». Для одного сервера и небольшой команды .env — разумный минимум. Для инфраструктуры с несколькими средами, аудитом доступа и требованием отслеживать, кто и когда обращался к секрету, стоит вырасти до полноценного секрет-менеджера — например, HashiCorp Vault, который умеет централизованное хранение, ротацию и выдачу временных, а не постоянных, учётных данных. Разворачивание Vault на своём VPS мы разбирали отдельно в статье «HashiCorp Vault: управление секретами».

Чтобы находка не повторилась в следующем коммите, стоит поставить барьер до, а не после факта — pre-commit hook, который прогоняет gitleaks на каждый коммит и блокирует его при обнаружении секрета: gitleaks protect --staged --verbose. Эту команду можно подключить через pre-commit framework или напрямую в .git/hooks/pre-commit — тогда попытка закоммитить пароль обрывается на локальной машине разработчика, а не превращается в очередную находку для следующего аудита через два года.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

С чего начать, если репозиторий огромный и истории двадцать тысяч коммитов?

Начните с gitleaks или trufflehog в режиме сканирования полной истории — это быстрее и надёжнее, чем ручной git log -p. Дайте инструменту отработать (может занять от нескольких минут до пары часов), а параллельно сделайте быстрый ручной grep по текущим файлам, чтобы закрыть очевидные критичные находки, не дожидаясь полного отчёта.

Можно ли просто удалить файл с секретами одним коммитом и не трогать историю?

Это уберёт секрет из текущего состояния, но не из истории — любой, у кого есть полный клон репозитория (старые бэкапы, копии бывших сотрудников, форки на других хостингах), сохранит доступ к удалённому значению. Единственная надёжная защита — сменить сам секрет; переписывание истории (git filter-repo, BFG) полезно как гигиена, но не заменяет ротацию.

Как отличить настоящий секрет от плейсхолдера без ручной проверки каждой строки?

Полностью автоматически — никак, но trufflehog с флагом --only-verified резко сокращает список за счёт активной проверки против реального сервиса там, где это возможно. Для внутренних паролей (собственная база, собственный сервис) такой проверки нет, их приходится разбирать глазами — но обычно таких находок на порядок меньше, чем строк, похожих на секрет по регулярке.

Стоит ли гонять сканер по всем сторонним библиотекам в vendor/ или node_modules/?

Как правило, нет смысла — секреты, зашитые в чужой опубликованный пакет, не ваша забота, и такие директории стоит явно исключать (--exclude-dir для grep, соответствующие опции у gitleaks/trufflehog), иначе результат утонет в шуме из тестовых фикстур.

Нужно ли пугать всю команду результатами первого скана?

Не обязательно превращать это в публичное разбирательство — цель методики не найти виноватого, а закрыть дыры. Соберите находки, расставьте приоритеты по критичности и радиусу поражения, смените боевые секреты в первую очередь, а разговор о процессе (pre-commit хуки, code review на секреты) ведите отдельно, без обвинительного тона — привычка хардкодить пароли обычно системная, а не персональная.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →