После отправки кода на облачный Mac система CI обычно сразу выполняет checkout, сборку и тестирование. Проблема в том, что имя и адрес электронной почты автора в Git-коммите — это всего лишь редактируемые текстовые поля. Они показывают, кто указан создателем коммита, но не доказывают, что объект действительно подписан этим разработчиком. Более надёжный подход — подписывать коммиты с помощью SSH, а затем поручить CI проверять весь диапазон от базового коммита слияния до текущего коммита по контролируемому списку открытых ключей.
Такая проверка не заменяет ревью кода и не определяет, безопасен ли сам код. Она решает более узкую, но важную задачу: подтверждает целостность Git-объектов, действительность подписей и принадлежность ключей тем, кому в данный момент разрешено отправлять код.
Сначала определите границы доверия при проверке
Проверка подлинности коммита включает как минимум три уровня:
- Соответствует ли подпись содержимому Git-объекта.
- Присутствует ли открытый ключ подписи в списке разрешённых подписантов.
- Сохранялись ли у этой личности права на отправку кода в момент создания коммита.
Первый уровень обеспечивается криптографической подписью, второй — поддерживаемым в репозитории списком, а третий по-прежнему зависит от принятого в команде процесса отзыва прав. Недостаточно просто выполнить git log --show-signature и увидеть сообщение «Good signature»: действующий, но не авторизованный ключ также способен создать корректную подпись.
Адрес электронной почты служит для отображения и уведомлений, открытый ключ — для проверки подлинности, а список разрешённых подписантов — для авторизации. Не следует считать их одним и тем же.
Список рекомендуется хранить в отдельном каталоге, изменения которого строго контролируются, например .ci/trusted_signers. Изменения самого списка должны проходить дополнительное ревью, чтобы автор не мог в рамках одного изменения добавить собственный открытый ключ и одновременно разрешить прохождение кода.
Настройка SSH-подписей коммитов в Git
Git версии 2.34 и новее поддерживает непосредственное подписание с помощью SSH-ключей. Сначала разработчик должен локально указать формат подписи и путь к открытому ключу:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgSign true
В конфигурации указывается файл открытого ключа, но сама подпись создаётся соответствующим закрытым ключом. После создания коммита можно убедиться, что объект действительно содержит подпись:
git cat-file commit HEAD | sed -n '/^gpgsig /,/^[^ ]/p'
Каждая строка файла разрешённых подписантов содержит идентификатор, необязательные ограничения и открытый ключ. В качестве идентификатора следует использовать стабильное командное обозначение, а не часто меняемое отображаемое имя:
ci-release namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
ios-team namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5BBBB...
После checkout репозитория в CI укажите Git путь к этому файлу:
git config --local gpg.format ssh
git config --local gpg.ssh.allowedSignersFile .ci/trusted_signers
git verify-commit HEAD
Не помещайте закрытые ключи в репозиторий. Для проверки в CI нужны только открытые ключи; закрытый ключ необходим лишь в среде, где создаются подписанные коммиты или теги.
Проверка полного диапазона коммитов
Проверять только HEAD — распространённая ошибка. Вредоносное или неавторизованное изменение можно спрятать в предыдущем коммите, а последним подписанным коммитом замаскировать итоговое состояние. Проверка должна охватывать все коммиты после доверенной базовой точки.
#!/bin/bash
set -euo pipefail
base="${MERGE_BASE_SHA:?missing MERGE_BASE_SHA}"
head="${HEAD_SHA:?missing HEAD_SHA}"
git cat-file -e "${base}^{commit}"
git cat-file -e "${head}^{commit}"
count=0
while IFS= read -r commit; do
git verify-commit "$commit"
count=$((count + 1))
done < <(git rev-list --reverse "${base}..${head}")
printf 'verified_commits=%s\n' "$count"
Значение MERGE_BASE_SHA должно вычисляться CI по целевой и сливаемой веткам либо передаваться из надёжного источника. Нельзя просто задавать его как HEAD~1. Для merge-коммитов также необходимо убедиться, что выбор базовой точки соответствует политике команды, иначе можно пропустить объекты, добавленные через второго родителя.
В результатах проверки следует фиксировать хеш коммита и этап, на котором произошёл сбой, но не копировать в общедоступные журналы полное сообщение коммита или переменные окружения. При ошибке лучше остановить сборку, чем продолжать создание артефакта с неподтверждённым происхождением: так аудит будет значительно проще.
Обработка неглубоких клонов, тегов и смены ключей
Неглубокие клоны часто приводят к ложным ошибкам скрипта: если базовый объект отсутствует локально, rev-list не сможет определить полный диапазон. Перед проверкой следует получить недостающую историю целевой ветки и явно убедиться в наличии базового объекта с помощью git cat-file -e. Если базовая точка не найдена, нельзя переходить к проверке только HEAD.
| Сценарий | Правильное действие | Недопустимое упрощение |
|---|---|---|
| Базовой точки нет в неглубоком клоне | Получить необходимую историю и заново вычислить диапазон | Проверить только последний коммит |
| Релизный тег | Использовать подписанный аннотированный тег и выполнить git verify-tag |
Проверить только имя тега |
| Переход со старого ключа на новый | На короткий срок оставить оба открытых ключа | Сразу заменить ключ, из-за чего старые задания перестанут проходить |
| Компрометация ключа | Немедленно удалить ключ и повторно проверить подписанный им диапазон | Изменить только отображаемую личность |
| Участник покинул команду | Удалить его запись из списка разрешённых подписантов | Только закрыть доступ для повседневного входа |
Следует заранее решить, должны ли исторические коммиты продолжать проходить проверку после удаления ключа. Самый простой строгий режим — проверка по текущему списку; он подходит для контроля слияний. Если требуется долгосрочная проверка исторических релизов, необходимо хранить записи доверия с периодами действия, а также архивировать релизный тег, хеш коммита и использованную на тот момент версию списка.
Предварительная проверка и диагностика сбоев
Прежде чем начать блокировать слияния, можно ввести период наблюдения: регистрировать ошибки, но при этом не допускать выпуск неподписанных релизов. В этот период особое внимание следует уделить следующим пунктам:
- Версии Git у разработчиков поддерживают SSH-подписи.
- Для обычных, merge- и автоматически созданных коммитов можно однозначно определить подписанта.
- CI получает полную базовую историю, а не полагается на фиксированную глубину клонирования.
- Изменения файла разрешённых подписантов проходят независимое ревью.
- Релизные теги и коммиты, на которые они указывают, проверяются отдельно.
- Учётные записи ботов используют отдельные ключи и не делят их с людьми.
- Для смены и компрометации ключей, а также ухода участников из команды предусмотрены выполнимые процедуры.
При диагностике сначала выполните git verify-commit --raw <hash>, чтобы различить ситуации «объект не подписан», «подпись повреждена» и «открытого ключа нет в списке разрешённых». Затем проверьте настройки репозитория gpg.format и gpg.ssh.allowedSignersFile, а также пространство имён, тип ключа и переводы строк в списке. Это позволяет отделить проблемы авторизации личности от неполной истории Git и не переподписывать без конца один и тот же коммит.
Когда поэлементная проверка коммитов, проверка тегов и ревью изменений списка становятся частью конвейера, система сборки получает проверяемую цепочку происхождения. Она не доказывает отсутствие дефектов в коде, но позволяет точно установить, какие объекты были подписаны тем или иным разрешённым ключом и почему неподтверждённые объекты не попали на последующие этапы сборки.
Часто задаваемые вопросы
Достаточно ли правильного адреса автора для доверия коммиту?
Нет. Имя и адрес автора являются обычными текстовыми полями и могут быть заданы кем угодно. Подпись подтверждает владение закрытым ключом, а список подписантов связывает ключ с разрешённой личностью.
Можно ли проверять только последний коммит ветки?
Нет. Любой коммит после доверенной базы влияет на итоговое дерево, поэтому git verify-commit должен выполняться для всего диапазона. Подписанный тег выпуска отдельно проверяется через git verify-tag.
Как сменить ключ подписи без остановки сборок?
Сначала добавьте новый открытый ключ в версионируемый список, временно разрешите оба ключа, проверьте коммиты с новым ключом и только затем удалите старый.
Настройте облачный Mac для следующей задачи разработки
Выберите Oak M4, Oak M4 Plus или Oak M4 Pro и настройте тариф аренды на одном из пяти физических узлов с учетом расположения команды.