Инженерная статья

Изоляция учётных данных Git в облачном Mac CI

Изоляция учётных данных Git в облачном Mac CI

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

Сначала выясните, где сохраняются учётные данные

Не спешите изменять конвейер. Сначала определите, какие настройки фактически читает Git, не выводя при этом секретные значения. Параметр --show-origin также показывает источник каждой настройки, позволяя различать системную, пользовательскую и локальную конфигурацию репозитория.

set +x

git config --show-origin --get-all credential.helper || true
git config --show-origin --get-regexp '^(credential|http)\.' || true
git config --show-origin --get-regexp '^url\..*\.insteadof$' || true

Особое внимание уделите трём признакам:

  • указывает ли credential.helper на постоянную связку ключей;
  • записан ли http.extraHeader в глобальную конфигурацию или конфигурацию репозитория;
  • переписывает ли url.*.insteadOf обычный адрес в URL с данными аутентификации.

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

remote_url="$(git remote get-url origin)"
case "$remote_url" in
  *://*@*)
    printf '%s
' "Remote URL contains embedded credentials" >&2
    exit 1
    ;;
esac

Главное правило скрипта аудита — сообщать только о возможном наличии учётных данных. Не выводите сами учётные данные в журнал сборки ради доказательства проблемы.

Создавайте отдельный HOME для каждого задания

Git определяет путь к пользовательской конфигурации на основе HOME. Если все задания используют HOME учётной записи runner, они совместно используют .gitconfig, настройки credential helper и состояние множества других инструментов. Более надёжный подход — создавать для каждого задания временный HOME с правами 700 и явно задавать файл глобальной конфигурации Git.

set -eu

ORIGINAL_HOME="$HOME"
JOB_HOME="$(mktemp -d "${TMPDIR%/}/git-job.XXXXXX")"
chmod 700 "$JOB_HOME"

export HOME="$JOB_HOME"
export XDG_CONFIG_HOME="$JOB_HOME/.config"
export GIT_CONFIG_GLOBAL="$JOB_HOME/.gitconfig"
export GIT_TERMINAL_PROMPT=0

mkdir -p "$XDG_CONFIG_HOME"

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

Не размещайте репозиторий во временном HOME

HOME задаёт границу для учётных данных и состояния инструментов, но не обязан одновременно служить рабочей областью. Исходный код должен оставаться в рабочем каталоге runner, чтобы сохранялась возможность контролировать объём данных и собирать артефакты. При таком разделении очистка HOME не удалит результаты сборки, а очистка рабочей области не оставит пользовательские настройки аутентификации.

Если конвейер выполняет задания параллельно, каждый параллельный слот должен отдельно вызывать mktemp. Нельзя повторно использовать фиксированный каталог, сформированный по имени репозитория. После аварийного завершения такой каталог унаследует следующее задание; кроме того, два задания могут одновременно изменять один файл .gitconfig.

Передавайте токен только по запросу Git

Токен должен передаваться в окружение через механизм секретных переменных CI, а скрипт должен ссылаться только на имя переменной. Не добавляйте токен в URL клонирования и не сохраняйте заголовок аутентификации надолго с помощью git config --global http.extraHeader.

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

: "${CI_GIT_USER:?CI_GIT_USER is required}"
: "${CI_GIT_TOKEN:?CI_GIT_TOKEN is required}"

git config --global credential.helper ""
git config --global --add credential.helper \
'!f() {
  if [ "$1" = get ]; then
    printf "username=%s
password=%s
" \
      "$CI_GIT_USER" "$CI_GIT_TOKEN"
  fi
}; f'

Отключите вывод команд и переход к интерактивному режиму

Перед обращением к секретным переменным обязательно выполните set +x. В противном случае shell запишет в журнал команды уже после подстановки значений переменных. Параметр GIT_TERMINAL_PROMPT=0 не менее важен: если токен отсутствует или недействителен, задание должно явно завершиться с ошибкой, а не зависнуть на невидимом интерактивном запросе.

Также проверьте, не выводит ли оболочка сборки переменные окружения автоматически. Диагностические сообщения должны показывать только факт задания переменной — например, можно проверить, превышает ли длина строки ноль. Нельзя выводить содержимое переменной, заголовок аутентификации или полный удалённый URL.

В автоматизированных задачах, которым требуются права записи, следует использовать разные учётные данные для чтения исходного кода и отправки артефактов. Заданию, выполняющему только checkout, не нужны права записи. Задание публикации также не должно получать права за пределами целевого репозитория и необходимых операций.

Используйте trap для всех вариантов завершения

Команда rm -rf, размещённая только в конце скрипта, не сработает при промежуточной ошибке, завершении по тайм-ауту или ручной отмене. Сразу после создания временного каталога зарегистрируйте trap, а в функции очистки сначала удалите переменные, затем — HOME задания.

cleanup_git_credentials() {
  set +e
  unset CI_GIT_TOKEN CI_GIT_USER
  if [ -n "${JOB_HOME:-}" ] && [ -d "$JOB_HOME" ]; then
    rm -rf "$JOB_HOME"
  fi
}

trap cleanup_git_credentials EXIT HUP INT TERM

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

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

Сделайте проверку остаточных данных обязательным этапом сборки

Отсутствие ошибок в скрипте ещё не доказывает, что очистка выполнена успешно. Рекомендуется добавить проверку после завершения задания на внешнем уровне runner либо поручить исполнителю при освобождении ресурсов проверять следующее:

  • временный HOME удалён;
  • в .git/config рабочей области нет заголовков аутентификации и удалённых URL с данными пользователя;
  • журнал задания не содержит известных сигнатур секретных переменных;
  • среди источников конфигурации Git присутствуют только ожидаемая системная и текущая временная конфигурации;
  • последующее пустое задание не может прочитать учётные данные репозитория из предыдущего задания.

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

Если одну физическую ноду используют несколько команд, второй границей должна стать учётная запись runner. Временный HOME устраняет остаточные данные на уровне задания, а отдельные пользователи разделяют процессы, права доступа к файлам и связки ключей между разными доменами доверия. Только сочетание этих двух уровней позволяет не считать очистку скриптом единственной линией защиты.

Итоговый критерий приёмки прост: до запуска задания нет учётных данных, которые оно могло бы унаследовать; во время выполнения они предоставляются только по запросу; очистка срабатывает при любом варианте завершения; следующее задание не может обнаружить доказательства существования предыдущего токена. Только при выполнении этих условий учётные данные Git действительно принадлежат заданию, а не постоянно работающему облачному Mac.

Часто задаваемые вопросы

Почему недостаточно удалить переменную токена в конце задания?

Данные могли сохраниться в helper, удалённом URL, глобальной конфигурации или связке ключей. Каждый источник необходимо проверить и очистить отдельно.

Можно ли постоянным Mac CI worker использовать общую связку ключей?

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

Облачный Mac с эксклюзивным доступом

Выберите выделенную физическую машину для разработки, сборки и удалённого рабочего стола

Сравните три конфигурации на Apple Silicon и при оформлении заказа выберите узел, срок аренды и дополнительные параметры хранилища.

Выбрать тариф аренды