После того как команда разрешает внешним участникам отправлять PR, задача облачного Mac CI уже не сводится к вопросу о том, компилируется ли код. Сами сценарии сборки также становятся недоверенными входными данными: они могут читать переменные среды, просматривать домашние каталоги, изменять общие кеши и даже подменять инструменты, которые будут использоваться в последующих заданиях. Цель безопасности должна быть сформулирована однозначно: внешние PR могут проходить компиляцию и тестирование, но не должны получать доступ к учётным данным выпуска или напрямую передавать артефакты в официальный релиз.
Сначала разделите два контура доверия
Разделите задания на два контура: «проверка без секретов» и «доверенный выпуск». Первый принимает внешние PR и выполняет только разрешение зависимостей, компиляцию, статический анализ и тесты, не требующие настоящих учётных данных. Второй принимает только зафиксированные коммиты, которые уже прошли проверку и были объединены, и отвечает за подпись, архивирование и доставку.
На выделенных физических узлах VMArm для этих двух контуров можно создать отдельных пользователей macOS. Пользователь проверки не должен входить в группу администраторов, хранить сертификаты выпуска или иметь доступ к домашнему каталогу пользователя выпуска. Пользователь выпуска, в свою очередь, не должен повторно использовать сценарии, кеши или каталоги инструментов, доступные для записи пользователю проверки.
Изоляция системных пользователей не равнозначна границе виртуальной машины. Она существенно снижает риск случайного чтения учётных данных обычными сценариями и загрязнения файлов, но не заменяет обновления системы, принцип минимальных привилегий и ручную проверку недоверенного кода.
Рекомендуемые границы выглядят следующим образом:
| Компонент | Проверка без секретов | Доверенный выпуск |
|---|---|---|
| Источник кода | Зафиксированный коммит внешнего PR | Зафиксированный коммит проверенной ветки |
| Подпись кода | Отключена | Включена согласно процессу выпуска |
| Рабочая область | Создаётся заново для каждого задания | Отдельный каталог |
| Кеш | Может быть удалён, не пересекает границы доверия | Общий только для доверенных заданий |
| Артефакты сборки | Используются только для проверки | Доставляются после повторной сборки |
| Учётные данные | Не передаются | Передаются на короткое время по мере необходимости |
Создайте рабочую область с минимальными привилегиями
Не позволяйте runner постоянно выполнять сборки непосредственно в корне домашнего каталога пользователя. Создайте отдельный корневой каталог для заданий проверки и запретите другим локальным пользователям его чтение. В начале каждого задания создавайте временный каталог, а после завершения всегда удаляйте его независимо от результата.
set -euo pipefail
umask 077
RUN_ROOT="$HOME/ci-untrusted"
mkdir -p "$RUN_ROOT"
chmod 700 "$RUN_ROOT"
WORK_DIR="$(mktemp -d "$RUN_ROOT/job.XXXXXX")"
cleanup() {
chmod -R u+rwX "$WORK_DIR" 2>/dev/null || true
rm -rf "$WORK_DIR"
}
trap cleanup EXIT INT TERM
cd "$WORK_DIR"
printf 'user=%s\nworkspace=%s\n' "$(id -un)" "$WORK_DIR"
Перед выполнением rm -rf необходимо убедиться, что путь относится к временному каталогу, созданному текущим заданием. Нельзя формировать его простым добавлением имени ветки. Имя ветки может содержать косые черты, пробелы или специально подобранные символы: оно подходит для отображения, но не для прямого использования в пути к файлу без предварительной обработки.
После извлечения исходного кода запишите хеш коммита и выведите его в журнал. В дальнейшем доверенный контур должен повторно получать код именно по этому неизменяемому идентификатору, а не полагаться только на имя ветки, которое может продолжать перемещаться.
Перед сборкой убедитесь, что в среде нет секретов
«Секреты не используются намеренно» не означает, что задание не может их прочитать. Служба runner может унаследовать среду запуска, а файлы инициализации Shell — добавить токены. До вызова сценариев проекта следует проверить имена переменных и передать команде сборки только необходимые значения.
blocked='TOKEN|SECRET|PASSWORD|PRIVATE|SIGNING|AUTH|CREDENTIAL'
if env | cut -d= -f1 | grep -Eiq "$blocked"; then
printf '%s\n' "blocked environment variable detected" >&2
exit 70
fi
env -i \
HOME="$HOME" \
PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
TMPDIR="$WORK_DIR/tmp" \
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-derivedDataPath "$WORK_DIR/DerivedData" \
CODE_SIGNING_ALLOWED=NO \
build
Если проект зависит от менеджера пакетов или собственных инструментов, явно добавьте проверенные каталоги инструментов в PATH, а не наследуйте полную среду входа пользователя. Проверка имён переменных — лишь начальный барьер: конфиденциальные значения также могут находиться в конфигурационных файлах, связке ключей, настройках прокси и помощниках учётных данных Git. Поэтому среда пользователя проверки должна изначально оставаться пустой, а не очищаться от нескольких переменных непосредственно перед каждой сборкой.
Изолируйте зависимости и кеши
Внешний код может записать данные в кеш, которые затем прочитает доверенное задание, что создаёт риск загрязнения. Самый надёжный подход — разделить кеши по контурам доверия, а для внешних PR дополнительно изолировать их по репозиторию или номеру задания. Не разрешайте контуру проверки записывать данные в кеши SwiftPM, скомпилированных модулей или собственных бинарных инструментов пользователя выпуска.
После восстановления кеша проверьте владельца и права доступа, а также отклоните символические ссылки, ведущие за пределы рабочей области. Исполняемые инструменты желательно брать из базового каталога, доступного только для чтения. Сценарии, создаваемые проектом во время сборки, должны запускаться исключительно в текущей временной рабочей области.
Не переносите артефакты через границу доверия
Успешная компиляция внешнего PR доказывает лишь то, что соответствующий коммит прошёл проверку в заданных условиях. Она не подтверждает, что полученный архив пригоден для выпуска. Задание проверки может подменить этап сборки, изменить содержимое выходных данных или добавить в архив лишние файлы. Поэтому пакеты приложений, архивы и каталоги экспорта, созданные в контуре без секретов, не должны передаваться в доверенное задание выпуска.
Доверенный контур должен выполнить следующие действия:
- Получить хеш коммита, подтверждённый системой проверки.
- Повторно извлечь этот коммит в новой рабочей области пользователя выпуска.
- Убедиться, что текущий коммит полностью совпадает с утверждённым значением.
- Заново разрешить зависимости, используя доверенный или пустой кеш.
- Выполнить тесты и заново создать архив из исходного кода.
- Предоставить учётные данные только на короткий период, когда требуется подпись.
- Записать хеш коммита, контрольную сумму файла блокировки зависимостей и контрольную сумму итогового артефакта.
Если из контура проверки необходимо передать отчёты о тестировании, передавайте только неисполняемые данные и разбирайте их на доверенной стороне по строгому формату. Не передавайте сценарии Shell, плагины, кеши компиляции или файлы, которые можно напрямую включить в пакет приложения.
Включите очистку после сбоев и аудит в критерии приёмки
Чаще всего изоляция нарушается именно в ветках обработки ошибок. После отмены сборки, превышения времени выполнения процесса или перезапуска машины могут остаться временные каталоги, фоновые процессы и журналы. В начале каждого задания runner должен проверять остаточные данные от предыдущего запуска, а на завершающем этапе — останавливать дочерние процессы, относящиеся к текущему заданию.
Перед вводом в эксплуатацию проверьте систему по следующему списку:
- Пользователь проверки не может просматривать содержимое домашнего каталога пользователя выпуска.
- Журналы внешних PR не содержат токенов, закрытых ключей или путей к учётным данным.
- В контуре проверки задано
CODE_SIGNING_ALLOWED=NO. - Два контура не используют общие доступные для записи кеши DerivedData и инструментов.
- После отмены задания временная рабочая область всё равно удаляется.
- Доверенный выпуск повторно извлекает зафиксированный коммит, а не использует рабочую область проверки.
- Для артефакта выпуска можно установить соответствующие хеш коммита, контрольную сумму файла блокировки и запись о сборке.
- Учётная запись runner не имеет административных прав, не требующихся для повседневной работы.
В завершение проведите активную проверку: отправьте тестовый PR, сценарий сборки которого пытается прочитать типичные конфиденциальные переменные, получить доступ к каталогу выпуска и записать файл в общий кеш. Правильный результат состоит не в том, что сценарий «ничего не нашёл», а в том, что эти ресурсы недоступны ему по архитектуре прав и процесса. Только после этого проверку внешних вкладов можно считать действительно отделённой от официального выпуска.
Часто задаваемые вопросы
Достаточно ли отключить подпись кода для внешнего PR?
Нет. Нужно также исключить секретные переменные, разделить пользователей, каталоги и кэши, ограничить права и проверить журналы на утечки.
Можно ли публиковать архив, созданный во время проверки внешнего PR?
Нет. После утверждения следует заново получить точный коммит и собрать архив в доверенной среде, не перенося бинарные файлы из недоверенного задания.
Выберите облачный Mac, который будет постоянно доступен для следующей сборки Xcode
Проверьте чип, память, хранилище, расчётный период и регион узла, затем перейдите к настройке. Фактическая доступность отображается в консоли в реальном времени.