Webrium
Подписаться

Секреты в CI: пять типовых утечек

Разберём, почему хранение секретов в хранилище и маскирование логов не спасают от утечки, и покажем пять мест, где конвейер CI теряет ключи

Цепочка шагов конвейера CI: чужой код входит, исполняется с ключами, и последний шаг — публикация — штатно выводит секрет за границу доверияграница довериясекрет наружу

Утечка секрета в CI почти никогда не начинается со взлома. Она начинается с того, что система делает ровно то, для чего её построили: исполняет чужой код с вашими ключами в руках и публикует всё, что при этом произвела. Ниже — где такая конструкция течёт, почему первое защитное решение её не закрывает и что мы считаем единицей защиты.

Как устроена ловушка

У любой CI-системы три свойства, из которых следует всё остальное.

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

Исполнению нужны полномочия. Чтобы собрать и проверить, джобе требуются токены пакетных реестров, ключи деплоя, учётки облачных провайдеров.

Всё, что джоба произвела, сохраняется и показывается. Лог, артефакт, кэш — по умолчанию достаточно широкому кругу читателей.

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

Отдельно зафиксируем роль хранилища секретов, оно ещё всплывёт. Хранилище — сильная штука: значения шифруются, доступ разграничен, в логах секрет отображается звёздочками. Но хранилище отвечает на вопрос «где секрет лежит», а не на вопрос «кто его держит». В момент старта джобы система раздаёт секреты в окружение каждого шага — в открытом виде. С этой секунды всё, что верно про среду исполнения, верно и про секрет.

Тупик: спрятать в хранилище и включить маску

Первое, что пробуют с секретами в CI, везде одно и то же: вынести значения из кода в хранилище системы и включить маскирование логов. Ход разумен, он стоит первым в любой инструкции, и после него приходит ощущение законченной работы: секреты ушли из репозитория, в логах звёздочки, галочки в чек-листе закрыты.

Это тупик, и вот почему.

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

Маска решает задачу отображения, причём самым дешёвым способом: фильтр ищет в готовом логе точную строку секрета и подменяет её звёздочками. Ключевое слово — «точную». Любое преобразование строки фильтр не видит: другая кодировка, разрыв посередине, экранирование внутри URL, запись в файл с последующей печатью файла. И главное — маска применяется к логу, а не к среде: всё, что секрет успел сделать до того, как попал в лог, осталось вне её поля зрения.

Хуже всего в этой паре не техническая дыра, а чувство защищённости. Звёздочки читаются как «секрет под контролем», в то время как он уже побывал в руках стороннего шага, уехал в артефакт или осел на раннере. Пять мест, где именно это происходит, — ниже.

Из той же серии второй ход: «перенесём сборку на свои раннеры, в закрытый контур». Без изоляции джоб друг от друга это не защита, а переезд утечки на этаж ближе к себе: чужой остаток теперь лежит не где-то, а на вашей машине.

После тупика вопрос меняется. Не «где хранить секрет», а «кому, когда и на сколько его выдавать».

Пять мест, где конструкция течёт

Логи: у маски слепые зоны

Печать своей строкой — не единственный путь секрета в лог. Пакетный менеджер при падении выводит свою конфигурацию целиком. Тестовый фреймворк при падении вываливает окружение. Режим отладки существует ровно для того, чтобы печатать всё, и его включают на минутку — «только глянуть, в чём дело».

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

Форк, который исполняется с ключами базового репозитория

Это не чья-то ошибка, а компромисс, зашитый в конструкцию.

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

Есть слой тоньше. В таком событии недоверенным неизбежно оказывается одно из двух: определение пайплайна или сам патч. Разные системы выбирают по-разному, и выбор описан в документации — в том абзаце, который никто не читает. Конфигурация, скопированная из одной системы в другую, приносит с собой чужое допущение о том, чему верить.

Два разных вопроса — «какой код проверить» и «кому верить» — система пытается закрыть одним переключателем. Здесь и рвётся.

Артефакты и кэш: секрет, который уехал

Секрет редко остаётся в окружении — он попадает в продукты сборки. Сборщик фронтенда упаковывает локальный файл с переменными прямо в бандл, потому что тот попал под шаблон включения. Файл блокировок хранит токен в URL реестра. Сгенерированный конфиг содержит пароль. Аргументы сборки образа контейнера записываются в историю слоёв и читаются из готового образа так же спокойно, как из лога.

Артефакт при этом часто доступен всей организации: «внутренний» не значит «секретный», а доступ к хранилищу раздают щедро. Кэш — общее состояние, разогретое в одном контексте и отданное в другом: кэш, собранный в защищённой ветке, достаётся ветке без защиты, а кэш, отравленный снаружи, попадает в доверенную сборку.

Бага тут нет: переносить данные через границы — прямая функция хранилища артефактов. Утечка — это фича, работающая как задумано.

Раннер, который помнит

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

Скрипты очистки не спасают: очистка — это код, код молча падает, а запускается с теми же полномочиями. Честная граница — свежая машина на каждую джобу; всё остальное — договор об уборке, который кто-нибудь однажды забудет продлить.

Чужой шаг в вашем пайплайне

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

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

Позиция редакции

Мы считаем: секрет, выданный в окружение джобы, доступен каждому, кто умеет исполнять код в этом CI. Защищаться надо исходя из этого, а не из звёздочек в логе.

Единица защиты — момент выдачи. Секрет получает конкретный шаг под конкретную задачу и живёт недолго; шаг, которому секрет не нужен, его не видит. Современные системы это умеют: переменные можно выдавать шагу, а не джобе, а токен джоба получает сама у провайдера — короткоживущий и узко ограниченный.

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

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

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

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

Цепочка поставок: перечень, который кто-то читает

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

Два маршрута обновления зависимостей: короткий путь откладывания обходится дорого, длинный путь регулярных обновнений — дёшевоОтложить — дорогоОбновлять — дёшево
Практика

Политика обновления зависимостей, которую соблюдают

Матрица дедлайнов и «день обновлений» заводят в тупик. Порядок действий для политики, в которой обновлять зависимости дешевле, чем откладывать.

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

Токен в браузере: где его хранить

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

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

Безопасность как метрика команды

Спорит с привычным счётчиком: количество найденных уязвимостей выбирает поведение команды, и обычно не то, которое нужно.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть