Репозиторий Persona готовился к публикации. Перед этим шагом полагается проверить историю на секреты — то, что до сих пор было личным проектом на личной машине, становится доступным всем, и вместе с текущим состоянием кода открывается вся его история.
Проверка нашла не то, что ожидалось. История коммитов оказалась чистой. А в рабочем каталоге, в обычном рабочем документе передачи дел, лежал живой тестовый пароль и подробное описание сетевой топологии: какие туннели подняты, к каким машинам, под какими именами.
Ниже — что именно делалось, в каком порядке, что было сделано неправильно и какой из этого получился регламент.
#Хронология
Отметки времени — не хронометраж происшествия, а бюджет, которого стоит держаться. Порядок шагов важнее их длительности.
| Минуты | Действие | Почему именно здесь |
|---|---|---|
| 0–3 | Отозвать или сменить сам секрет у поставщика | единственное действие, которое реально отменяет утечку |
| 3–5 | Проверить, что новый секрет работает, а старый — нет | иначе вы не знаете, отозвали ли вообще |
| 5–15 | Найти все места, где лежит копия | список ниже; их всегда больше, чем кажется |
| 15–30 | Проверить журналы доступа у поставщика | было ли использование, откуда, когда |
| 30–60 | Убрать секрет из репозитория и документов | и только теперь |
| позже | Поставить защиту, чтобы это не повторилось | хук, тест, игнор-правила |
Самое важное здесь — порядок первой и предпоследней строки. Интуиция требует сначала «убрать», а потом разбираться. Интуиция ошибается.
#Что мы сделали неправильно
Считали проверенной только историю git. Аудит гонялся по коммитам, и он был чистым — все 568 штук на тот момент. При этом секрет лежал в файле, который в этих же коммитах и находился, просто вне тех шаблонов, которые проверялись. Проверять надо и историю, и текущее дерево, и делать это разными проходами.
Держали в репозитории документ, который никто не считал секретным. Файл передачи дел — это по определению место, куда пишут «как всё устроено»: адреса, имена машин, топология туннелей, а в нашем случае и пароль от тестового стенда. Никто не воспринимает такой файл как хранилище секретов, потому что он «просто заметки». Это и есть механизм утечки: секрет попадает не в конфиг, а в прозу.
Тестовый пароль считали не настоящим. Он был настоящим: он открывал реальный стенд, поднятый на реальной машине, к которой был доступ снаружи. Слово «тестовый» в имени переменной не меняет того, что этим значением можно воспользоваться.
Описание топологии не считали чувствительным вообще. Пароль хотя бы очевиден. Описание того, какие туннели куда подняты и как называются машины, — это карта, которая экономит нападающему всю разведку. Утечка знания об инфраструктуре не выглядит как утечка и потому не чинится.
#Что сработало
Отзыв раньше уборки. Значение перестало быть валидным до того, как кто-либо начал переписывать файлы. Пока секрет действителен, всё остальное — косметика.
Полный проход по дереву, а не по последним коммитам. Проверка одной и той же логикой в двух режимах: по подготовленным к коммиту изменениям и по всему отслеживаемому дереву. Первое быстро и на каждый коммит, второе медленно и на каждый прогон тестов.
Расширение игнор-правил с имён на шаблоны. Было перечисление конкретных файлов, стало перечисление классов: .env*, *.db, *.pem, *.session, логи. Именованное правило защищает от уже известного файла; шаблон — от того, который появится завтра.
Заполненный шаблон переменных окружения. Каждая секретная переменная, встречающаяся в коде, документирована в шаблоне с пустым значением. Недокументированная переменная — прямая дорога к захардкоженному ключу: человек не нашёл, куда класть значение, и вписал его в код.
Как это ставится технически — хук, тест, набор шаблонов поиска — в статье про секреты в проекте и защиту от коммита.
#Где ещё могла остаться копия
Это главная часть чек-листа и самая недооценённая. Секрет копируется гораздо охотнее, чем кажется.
- История команд оболочки.
~/.bash_history,~/.zsh_history, журнал PowerShell (ConsoleHost_history.txt). Любой ключ, который вы хоть раз вставили в командную строку, лежит там открытым текстом. - Переменные окружения запущенных процессов. Перезапустите то, что читало старое значение, иначе процесс продолжит им пользоваться.
- Файлы конфигурации инструментов.
~/.netrc,~/.npmrc,~/.pypirc, конфиги CLI облачных провайдеров. - Резервные копии рабочей машины. Копия репозитория месячной давности содержит секрет в прежнем виде.
- Экспорт базы приложения. Если ключи лежат в базе — а в Persona ключ участника лежит в таблице настроек открытым текстом, — то любой полный экспорт базы это копия всех ключей. Обращаться с таким файлом надо как с хранилищем паролей и никогда не прикладывать его к обращению в поддержку.
- Журналы приложения и веб-сервера. Тело запроса, попавшее в лог, содержит всё, что в нём было.
- Переписка. Секрет, отправленный себе в мессенджер «чтобы скопировать на сервер», живёт там вечно.
- Кэш систем сборки и артефакты CI. Переменные сборки печатаются в журналы чаще, чем хотелось бы.
- Форки и клоны репозитория. Переписывание истории в вашей копии не трогает чужие.
Пройдитесь по списку целиком, а не по трём очевидным пунктам. Практика показывает: находится минимум одно место, о котором не подумали.
#Выводы и регламент
Регламент на будущее уместился в семь строк.
- Считать утёкшим всё, что попало в коммит, — даже локальный, даже на секунду. Ротация дешева, разбирательство после публикации — нет.
- Отзывать до уборки. Всегда.
- Проверять историю и дерево разными проходами, одним и тем же набором правил.
- Держать документы передачи дел под тем же режимом, что и код. В них секретов оказывается больше, чем в конфигах.
- Считать описание инфраструктуры чувствительным. Имена машин, адреса, топология туннелей — это не документация, это разведданные.
- Ставить защиту до ошибки. Хук плюс тест, который работает даже там, где хук не установили.
- После инцидента — посмотреть журналы доступа у поставщика. Отзыв закрывает будущее, журнал отвечает на вопрос про прошлое.
Отдельный пункт, который стоит держать в голове при выборе архитектуры: чем больше секретов у вас на руках, тем дороже такой инцидент. Ассистент, работающий на локальной модели, вообще не требует ключа провайдера — это не панацея, но это на один класс секретов меньше. Разбор того, какие угрозы при этом остаются, — в статье про угрозы локального ИИ. Где хранить ключ, если он всё-таки нужен, — в разборе безопасного хранения API-ключа.
#Частые вопросы
#Помогает ли удалить файл новым коммитом?
Нет. Старый объект остаётся достижимым в истории, и его находят автоматические сканеры репозиториев. Помогает переписывание истории — если ветка ещё никуда не отправлена. Если отправлена, считайте секрет опубликованным и отзывайте его.
#Что делать, если утёк ключ облачного провайдера модели?
Отзовите его в кабинете провайдера в первую очередь — там же обычно видно потребление, по которому понятно, воспользовался ли им кто-то. Дальше — новый ключ, обновление в настройках приложения и проход по чек-листу мест, где могла остаться копия. Учтите, что пока ключ был валиден, счёт мог расти на чужих запросах.
#Нужно ли переписывать историю, если репозиторий закрытый?
Если он останется закрытым навсегда — формально нет, но вы этого не знаете заранее. Репозиторий Persona был закрытым, и именно подготовка к возможной публикации заставила проверить историю. Проще один раз переписать, пока веток мало, чем разбираться потом.
#Как понять, воспользовался ли кто-то утёкшим ключом?
По журналам доступа со стороны поставщика: время запросов, адреса, объём. Собственные логи здесь помогают мало, потому что чужое использование ключа идёт мимо вашей машины. Если журналов у поставщика нет — исходите из худшего.
#Что делать пользователю сервиса, если утечка случилась не у него?
Сменить пароль, отозвать выданные токены и, если сервис это позволяет, потребовать удаления своих данных. Порядок действий и что именно удаляется — в статье про удаление аккаунта и данных.
#Стоит ли из-за таких историй отказаться от самостоятельного хостинга?
Не обязательно, но стоит понимать цену. При самостоятельном хостинге вы единственный, кто заметит утечку и отреагирует на неё, — и это лишь один пункт из списка. Честный перечень остальных — в статье про риски самостоятельного хостинга, и её лучше прочитать до, а не после того, как вы подняли свой ИИ-сервис.