Когда один облачный Mac одновременно получает код, выполняет сборку, тестирование и публикацию, чаще всего упускают из виду границу не между каталогами, а между средами процессов. После того как конвейер записывает токен в переменную среды, xcodebuild, скрипты, хост тестов и фоновые дочерние процессы по умолчанию наследуют её. Даже если основное задание уже завершилось, неостановленный вспомогательный процесс может по-прежнему хранить полный набор переменных. Правильный подход состоит не в отказе от переменных среды, а в максимальном сокращении времени и числа процессов, которым доступен секрет.
Составьте карту распространения переменных
Каждый процесс, запускаемый из runner, следует отнести к одной из четырёх категорий: сборка, тестирование, публикация или вспомогательные инструменты. Для сборки и тестирования токен публикации обычно не нужен, а вспомогательные инструменты также не должны его наследовать. Зафиксируйте имя переменной, этап внедрения, использующую её команду и ожидаемый момент удаления, но не записывайте реальное значение.
| Объект проверки | Типичный риск | Способ проверки |
|---|---|---|
| Родительский процесс runner | Получает все секреты при запуске | Проверять только наличие имени переменной |
| Дочерний процесс сборки | Безусловно наследует среду родителя | Проверять наследование с помощью случайного маркера |
| Домен launchd | Сохраняет значение между заданиями после setenv |
Проверять и очищать до и после задания |
| Временные файлы | Слишком широкие права или остатки после аварийного завершения | Установить права 600 и удалять через trap |
| Журналы | Трассировка Shell выводит аргументы команд | Отключать set -x на этапе работы с секретами |
Скрипт аудита должен отвечать только на вопрос, виден ли определённый маркер. Не сохраняйте снимок среды как артефакт сборки. Полный вывод
env,exportилиps ewwсам по себе может стать новым источником утечки.
Проверьте наследование случайным маркером
Не используйте для эксперимента настоящий токен. Создайте одноразовый маркер, запустите недолговечный дочерний процесс и без вывода значения проверьте, видит ли он этот маркер. Следующая проверка не печатает содержимое маркера в журнал, но по коду завершения конвейер сможет определить, произошло ли наследование.
set -euo pipefail
marker="$(/usr/bin/uuidgen)"
export CI_SECRET_PROBE="$marker"
/bin/sleep 20 &
child_pid=$!
if /bin/ps eww -p "$child_pid" -o command= | /usr/bin/grep -q "$marker"; then
result=1
else
result=0
fi
kill "$child_pid" 2>/dev/null || true
wait "$child_pid" 2>/dev/null || true
unset CI_SECRET_PROBE
test "$result" -eq 1
Этот тест подтверждает, что наследование по умолчанию действительно происходит. Он не означает, что в рабочих журналах нужно постоянно сканировать все процессы. Для приёмочной проверки можно использовать постоянное имя переменной со случайным значением и записывать только результат: «успешно» или «ошибка».
Проверьте остатки в launchd
Не следует внедрять секреты задания через launchctl setenv, поскольку срок их жизни может превышать длительность отдельного сеанса Shell. Если прежние скрипты использовали такой способ, сначала проверьте состояние без вывода значения, а затем выполните:
launchctl unsetenv CI_RELEASE_TOKEN
test -z "$(launchctl getenv CI_RELEASE_TOKEN)"
Проверку нужно выполнять дважды: в начале задания и на этапе очистки. Если runner работает от отдельного пользователя, команды необходимо запускать в соответствующем этому пользователю домене launchd, а не ограничиваться проверкой текущего удалённого сеанса.
Создайте минимальную среду для сборки
Команда env -i позволяет запустить процесс с пустой средой, однако очищать её вслепую нельзя. Инструментам Xcode обычно нужны HOME, PATH, TMPDIR, настройки локали и явно заданный каталог разработчика. Сначала проверьте среду запросом версии, а затем запускайте полную сборку.
set -euo pipefail
job_tmp="$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/mac-ci.XXXXXX")"
trap '/bin/rm -rf "$job_tmp"' EXIT HUP INT TERM
/usr/bin/env -i \
HOME="$HOME" \
PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
TMPDIR="$job_tmp/" \
LANG="en_US.UTF-8" \
DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer" \
/usr/bin/xcrun xcodebuild -version
Если реальному проекту нужны Ruby, Node.js или менеджеры пакетов, добавляйте каталоги их исполняемых файлов в PATH по отдельности, не восстанавливая всю среду входа Shell. Для каждой добавленной переменной укажите, какой именно команде она требуется.
Разделите сборку и публикацию на две области доступа
На этапе сборки создаются только архив и проверочные файлы; учётные данные для публикации ему недоступны. Этап публикации получает уже проверенный артефакт и внедряет необходимые переменные только в команду загрузки. Не выполняйте export CI_RELEASE_TOKEN в начале задания, иначе токен унаследуют все последующие скрипты.
set +x
CI_RELEASE_TOKEN="$release_token" \
/usr/bin/env \
RELEASE_ARTIFACT="$artifact_path" \
./ci/publish.sh
unset release_token
set -x
Более надёжный вариант — настроить publish.sh так, чтобы он запускал только один процесс загрузки, дожидался его завершения и сразу после этого прекращал работу. Если инструменту нужен файл с учётными данными, используйте отдельный временный каталог задания, перед созданием файла задайте umask 077, а после записи убедитесь, что права равны 600. Не помещайте такой файл в репозиторий, на рабочий стол пользователя или в каталог долговременного кэша.
Ограничьте обработчик сбоев
Обработчики сбоев часто собирают сведения о среде, процессах и рабочем пространстве. Они не должны выполнять неотфильтрованный env или архивировать полные командные строки. Допустимо собирать код завершения, версии инструментов, путь к артефакту, объём свободного места на диске и имена переменных, прошедшие фильтрацию по белому списку. Если нужно проверить секрет, сравнивайте только хеш или наличие случайного пробного значения, не сохраняя исходное значение.
Сделайте результат очистки обязательным условием задания
Успешное выполнение задания не означает успешную очистку. На завершающем этапе необходимо как минимум проверить, что переменные публикации удалены командой unset, в launchd не осталось соответствующих имён, временный каталог больше не существует, дочерние процессы тестирования и загрузки завершены, а трассировка команд в журнале отключена. Если хотя бы одна проверка не пройдена, текущий runner следует пометить как требующий проверки и прекратить назначать ему задания публикации с секретами.
Рекомендуется создавать отдельный TMPDIR для каждого задания и очищать процессы целыми группами. Если остановить только основной скрипт, могут продолжить работу вспомогательные процессы симулятора, загрузчик или пользовательский демон. После очистки ещё раз запустите случайную пробу без реального значения и убедитесь, что новый дочерний процесс не видит маркер с предыдущего этапа.
Конечная цель состоит не в полном отказе от переменных среды в конвейере, а в создании чётких границ: на этапе сборки нет секрета публикации, на этапе публикации секрет видит только один необходимый процесс, а после завершения задания он не сохраняется ни в процессах, ни в launchd, ни на диске. Если включить эти проверки в скрипты инициализации и завершения работы runner, следующим проектам не придётся полагаться на память оператора.
Часто задаваемые вопросы
Достаточно ли хранить секреты CI только в переменных среды?
Нет. Переменные наследуются дочерними процессами и могут попасть в диагностический вывод или долгоживущую службу. Передавайте секрет только конкретной команде, которой он действительно нужен.
Как проверить отсутствие секретов после завершения задания?
Проверьте родительский процесс runner, домен launchd и оставшиеся дочерние процессы по имени переменной, затем убедитесь, что временный каталог удалён. Само значение выводить в журнал нельзя.
Может ли env -i нарушить работу xcodebuild?
Да, если не передать обязательные значения. Явно задайте HOME, PATH, TMPDIR, LANG и DEVELOPER_DIR, проверьте версию инструмента и небольшую сборку, а затем применяйте список ко всему конвейеру.
Перенесите следующую сборку в облачный Mac mini
Выберите конфигурацию, период оплаты и регион узла, чтобы запускать разработку, сборки и автоматизацию на выделенной физической машине.