Привет, Хабр! Я Алексей Шаманов, старший системный архитектор в ГК “Цифра”, занимаюсь проектированием решений на базе платформы ZIIoT. Сегодня предлагаю поговорить об использовании SSH для подписи коммитов в git.
Давным-давно, когда солнце было желтее, трава зеленее, а git действительно «децентрализованным», для определения/верификации авторства вносимых изменений использовался PGP.
То есть, совсем давно вообще ничего подобного не использовалось. И задачи-то в нашей хипстерской «И таааак сойдёт!» коммуне не было — прислали патч по почте, совпал email в коммите с адресом отправителя — замечательно, не совпал — разберёмся (Но это не точно!). Впрочем, во времена столь отдалённые, когда динозавры ещё ходили по земле, мы погружаться не будем и вернемся к PGP.
С одной стороны, задачу определения принадлежности набора изменений метод решал. И неотказуемость обеспечивал. И даже — чьорт побьери! — позволял более-менее подтвердить личность участника в децентрализованной среде без единого root-of-trust. Оборотной стороной являлась некоторая, гм, «хтоничность» настройки какой из вариантов (openpgp, gnupg) ни возьми, да и о каком-либо удобстве использования речь, в общем-то, не идёт.

В общем, сам собой сформировался некоторый консенсус. Штука хорошая, штука важная, штука нужная, но использовать её мы, конечно же, не будем. Ну только если нас прям не заставят. И ведь таки-да, заставляли. В крупных opensource-проектах двухфакторная аутентификация в SCM и commit signing стала, de facto, стандартом. В корпоративном окружении криптографическая подпись вносимых изменений — давно уже составная часть compliance policy безопасной разработки, но сколько-нибудь удобной подобная работа (Особенно в динамической среде с необходимостью работать из разных окружений) всё же не являлась.
С другой стороны, в наши дни git, de facto, — централизованная система с единым root-of-trust и задачу обеспечения доверия можно уже и не решать. Вот что у человека в свойствах профиля на github/gitlab написано, тому и верим. Это открыло возможность использовать для подписи SSH-ключи, которые в наше время есть примерно у всех.
Настройка подписи под Linux
Инструкций по настройке примерно миллион:
shaman@test.lc@haproxy:~$ ssh-keygen -t ed25519 -C "user@test.lc"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/shaman@test.lc/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/shaman@test.lc/.ssh/id_ed25519
Your public key has been saved in /home/shaman@test.lc/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:jfjSeEI4+SQMQMt24+q1mXJ72rb2xy5lxClcHein+rI user@test.lc
The keys randomart image is:
+--[ED25519 256]--+
|o. o.. |
|... o . |
| +.o . + . |
|. oo.o + B . |
| .* + S + |
| . + + |
| . . = O |
|....=+ B o |
| .o*ooEBo |
+----[SHA256]-----+
#Добавим ключ в ssh-agent
shaman@test.lc@haproxy:~$ eval $(ssh-agent -s)
Agent pid 1502
shaman@test.lc@haproxy:~/testrepo$ ssh-add ~/.ssh/id_ed25519
Enter passphrase for /home/shaman@test.lc/.ssh/id_ed25519:
Identity added: /home/shaman@test.lc/.ssh/id_ed25519 (user@test.lc)
echo "user@test.lc $(cat ~/.ssh/id_ed25519.pub)" >> ~/.ssh/allowed_signers
# Создадим репозиторий
shaman@test.lc@haproxy:~$ mkdir testrepo
shaman@test.lc@haproxy:~$ cd testrepo/
shaman@test.lc@haproxy:~/testrepo$ git init
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
Initialized empty Git repository in /home/shaman@test.lc/testrepo/.git/
# Сконфигурируем git для подписи коммитов
shaman@test.lc@haproxy:~/testrepo$ git config --global gpg.format ssh
shaman@test.lc@haproxy:~/testrepo$ git config --global user.signingkey ~/.ssh/id_ed25519.pub
shaman@test.lc@haproxy:~/testrepo$ git config --global user.name "Aleksey T. Shamanov"
shaman@test.lc@haproxy:~/testrepo$ git config --global user.email user@test.lc
shaman@test.lc@haproxy:~/testrepo$ git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
shaman@test.lc@haproxy:~/testrepo$ git config --global commit.gpgsign true
# Проверим подпись
shaman@test.lc@haproxy:~/testrepo$ echo "Test signing" > ./testfile
shaman@test.lc@haproxy:~/testrepo$ git add testfile
shaman@test.lc@haproxy:~/testrepo$ git commit -m "Test signing"
[master (root-commit) 5a7c088] Test signing
1 file changed, 1 insertion(+)
create mode 100644 testfile
shaman@test.lc@haproxy:~/testrepo$ git log --show-signature
commit 5a7c08803c5b2486907d0e051b4c80cbd7acd71a (HEAD -> master)
Good "git" signature for user@test.lc with ED25519 key SHA256:jfjSeEI4+SQMQMt24+q1mXJ72rb2xy5lxClcHein+rI
Author: Aleksey T. Shamanov <user@test.lc>
Date: Thu Apr 3 10:36:20 2025 +0500
Test signing
Единственная проблема, с которой я столкнулся, была связана с использованием аутентификатора YubiKey, с SSH-ключом, требовавшим подтверждения использования:
ssh-keygen -t ecdsa-sk -O resident -O application=ssh:mainkey -O verify-required
Добавление ключа в ssh-agent требовало явного ввода пин-кода и касания аутентификатора, а ssh-agent не мог этого сделать. Проблему решила установка пакета ssh-askpass. Осталось добавить ключ в github/gitlab, и… Стоп. А в чём, собственно, новизна работы? Точно! Кроме Linux’а есть же ведь еще и Windows! Надеюсь, у вас, как и у меня, уже 10/11 со встроенным OpenSSH, так что всё должно быть просто.
Настройка подписи под Windows
# Проверяем наличие сервиса
PS C:\Users\atsha> Get-Service -Name ssh-agent
Status Name DisplayName
------ ---- -----------
Stopped ssh-agent OpenSSH Authentication Agent
# Настраиваем автоматический запуск и запускаем
PS C:\Users\atsha> Set-Service ssh-agent -StartupType Automatic
PS C:\Users\atsha> Start-Service ssh-agent
# Проверяем, что все работает
PS C:\Users\atsha> Get-Service -Name ssh-agent
Status Name DisplayName
------ ---- -----------
Running ssh-agent OpenSSH Authentication Agent
# Далее аналогично создаем и добавляем ключ
ssh-add "C:\Users\atsha\.ssh\id_ed25519"
PS C:\Users\atsha> ssh-add -l
256 SHA256:... user@test.lc (ED25519)
Полностью аналогично настраиваем git:
git config --global gpg.format ssh
git config --global user.signingkey C:\Users\atsha\.ssh\id_ed25519.pub
git config --global user.name "Aleksey T. Shamanov"
git config --global user.email user@test.lc
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
git config --global commit.gpgsign true
Проверяем подпись… Первый облом. По умолчанию git использует забандленный OpenSSH, который про встроенный в Windows не знает, знать не хочет и работать с его ssh-agent’ом не умеет. Нет, можно, конечно, настроить поставляемый с git’ом ssh/ssh-agent, поправить .bashrc, но, ээээ… А зачем тогда мы всё вышеописанное делали?
Для использования «системного» OpenSSH есть аж целых три способа:
Переустановить git с использованием внешнего OpenSSH:

2. Явным образом указать «нужный» SSH, задав переменную окружения GIT_SSH_COMMAND
PS C:\Users\atsha> Get-Command ssh.exe
CommandType Name Version Source
----------- ---- ------- ------
Application ssh.exe 9.5.3.1 C:\Windows\System32\OpenSSH\ssh.exe
$env:GIT_SSH_COMMAND = C:\Windows\System32\OpenSSH\ssh.exe
3. Задать путь через конфигурацию: git config --global core.sshCommand "'C:\Windows\System32\OpenSSH\ssh.exe'"
Делаем, проверяем — error: Couldn't get agent socket. Ага. SSH есть, он правильный, но про сокет ssh-agent’а почему-то ничего не знает. А как, собственно, он про него узнает? А, да — по значению переменной SSH_AUTH_SOCK. Проверяем echo $env:SSH_AUTH_SOCK — ну да, пустота. Ok, Google — путь к сокету ssh-agent’а в Windows? Не то… Снова не то… Опять не то… О, вроде как pipe: \\.\pipe\openssh-ssh-agent Задаём значение сразу через системные переменные окружения (чтоб не мучиться с квотингом), проверяем:
PS C:\Users\atsha> echo $env:SSH_AUTH_SOCK
\\.\pipe\openssh-ssh-agent
PS D:\test> git commit -m 'Test signing'
[master (root-commit) f18245d] Test signing
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 testfile
PS D:\test> git log --show-signature
commit f18245d7df2fbed2c8e49b0b65dc52e435fbdd72 (HEAD -> master)
Good "git" signature for user@test.lc with ED25519 key SHA256:...
Author: Aleksey T. Shamanov <user@test.lc>
Date: Thu Apr 3 11:17:25 2025 +0500
Test signing
Voila?! Не-а! Есть ведь ещё и окружение WSL2.
Настройка подписи в WSL2
Каждая проблема имеет решение — простое, понятное и неправильное. Есть оно и у этой задачи — просто берём, копируем ключик в wsl-окружение (Да бог с ним, можно даже не копировать, просто путь указать!) и добавляем его в ssh-agent окружения и проблема решена! Правда назвать это «решение» сколько-нибудь «удобным», кмк, не получится: два раза вводить пароль от ключа при любых подергушках, два независимых SSH-агента — ну, такое себе. Хорошо бы вот ну… один использовать!
И да, возможность есть, и даже не одна! Итак, вариант первый — из палки и верёвки.
Использование npiperelay
В Windows в качестве «unix-socket’ов» используются named pipes, о которых ни Linux, ни WSL 2 примерно ничего не знают. Но мир не без добрых людей, и так появился проект npiperelay, позволяющий получать доступ к Windows named pipes как к unix socket’ам.
На стороне WSL установим socat и протестируем его вызов:
export SSH_AUTH_SOCK=$HOME/.ssh/auth.sock
setsid socat UNIX-LISTEN:"~/.ssh/auth.sock,fork" EXEC:"/mnt/d/npiperelay.ex. -ei -s //./pipe/openssh-ssh-agent"
>/dev/null 2>&1 &
shaman@hwhost:~$ ssh-add -l 256 SHA256:... (ED25519)
Та-дааам! Осталось только запихнуть это дело в .bashrc с проверкой существования сокета и можно пользоваться. Более того, «на сдачу» ещё и обычный ssh с ключом заработает!

И в общем всё в этом способе хорошо, но очень уж развесистая гирлянда выходит. Да и использование niperelay с последним коммитом, сделанным пять лет назад, некоторым образом напрягает. Можно ли обойтись без него? Можно!
Использование Windows OpenSSH в WSL
Обратили внимание на непосредственный вызов npiperelay.exe в прошлом способе? Вспомнили, что из WSL-окружения можно безболезненно вызывать приложения из основной системы? Ну, догадались?
Да, в общем-то никто нам не мешает в качестве core.sshCommand вызывать ssh.exe из состава Microsoft Windows. Уж он-то точно умеет со своим пайпом работать, мы проверяли! Единственный момент, который надо иметь в виду, — необходимость изменения путей под unix-like стандарт:
git config --global core.sshCommand /mnt/c/Windows/System32/OpenSSH/ssh.exe
git config --global user.signingkey /mnt/c/Users/atsha/.ssh/id_ed25519.pub
Работа с IDE
Всё? Ну… да, но нет. Не из консоли ж мы коммитить будем, правда? Нет, бывает что и из неё, но, in general, скорее всего из IDE, и вот тут вполне может быть засада. При создании проекта в окружении WSL IntelliJ автоматически определяет для работы git из окружения WSL:

И всё бы ничего, вот только запускает его IDE в обход твоего .bashrc с заботливо наколбашенным пробросом сокета и/или заменой SSH. Самое «умное», что пришло мне в голову для решения этой задачи, — сделать файлик-обёртку git следующего содержания:
#!/bin/bash
SSH_AUTH_SOCK=$HOME/.ssh/agent.sock /usr/bin/git "$@"
# Путь к сокету будет отличаться в зависимости от способа работы с ним из окружения WSL
…и явно прописать его в настройках. Решение корявенькое, но работает. Ещё одна минорная проблема, которая может возникнуть, — различие в правах WSL/Windows. WSL-окружение видит все файлы Windows-окружения с правами 777, что, очевидно, нервирует SSH. Если вы указываете в качестве user.signingkey путь к ключу в окружении Windows, «умный» SSH наотрез отказывается работать. В качестве решения можно использовать непосредственное задание публичного ключа вместо указания пути к нему:
git config --global user.signingkey "ssh-ed25519 AAAAC3 user@test.lc"
Ну, теперь-то уж точно всё? Практически. Остался достаточно редкий кейс с использованием всякого рода удалённых виртуалок.
Удалённая работа
Собственно, с одной стороны, никто нам не помешает просто скопировать ключи на удалённую машину, «после чего решение сведется к предыдущему варианту», но без нужды разбрасываться закрытым ключом по различным машинам — ну очень такое себе.
На этот случай есть вполне типовое решение от проекта OpenSSH — “SSH agent forwarding”, позволяющее пробросить сокет через SSH-соединение. Для настройки нам необходимо создать файл ~/.ssh/config (Да-да, под Windows вполне работает) и прописать в нём разрешение пересылки агента для требуемого хоста или хостов:
Host 192.168.1.61
HostName haproxy.test.lc
User shaman@test.lc
ForwardAgent
И подключиться к нему через ssh с указанием ключа -A:
ssh -l shaman@test.lc 192.168.1.61 -A
shaman@test.lc@haproxy:~$ ssh-add -l
256 SHA256:... user@test.lc (ED25519)
На сим с настройкой клиентской части мы закончили (варианты использования gitbash ssh\putty+pagent и т. д. не рассматриваем как очевидно маргинальные) и можно задуматься и о централизованном управлении ключами в корпоративной среде - но это уже са-аа-авсем другая история...
Комментарии (10)

shirmanov
05.12.2025 07:53То есть вам требуется защита от того, что в компании один действующий сотрудник с доступом к репозиторию делает комиты выдавая себя за другого действующего сотрудника с доступом?

atshaman Автор
05.12.2025 07:53Персонально мне - нет, но de-facto это уже стандартное требование к "контролю целостности", "неотказуемости", "защиты авторства" и т.д., кочующее от NIST к ФСТЕКу и далее по внутренним регламентам и реализованное на уровне возможности примерно во всех продуктах.
Не то, чтобы я хоть капельку думал, что ФСТЕК удовлетворит подпись ключом ed25519 - но лучше иметь что-то, чем одно большое ничего, тем более что стоит оно - около "бесплатно".

shirmanov
05.12.2025 07:53Таких требований нет. Достаточно давно была проблема, которую эта функция решала. Также как и pgp для email. Но сейчас такой проблемы нет. Это атавизм в гите. Поэтому и не понятно какую проблему вы решаете.
Причём тут ФСТЭК? вы им передаёте файлы целиком с контрольными суммами. Можно заменить на подписи, но требования нет. Авторство файлов всем безразлично. Это проблема юрлица заниматься отчужденим прав от разработчиков в свой адрес.
Если вы настраиваете внутреннее окружение для разработки, соответствующее требованиям, то вам требуется контролировать пользовательские девайсы, пользовательский доступ к ним, и доступ этих пользователей к репозиториям. Подписи коммитов вам в этом никак не помогают. Не имеет никакого значения какой git user какой коммит сделал.
alexovchinnicov
Подпись это половина дела, как на счет проверки этой самой подписи? Хотелось бы увидеть как реализована эта часть, используются pgp инфраструктура keyserver?
atshaman Автор
Exactly. Но как бы ээээ... вся эта бабуйня как раз таки и затевалась, чтобы не иметь примерно "никакого" отношения к инфраструктуре PGP.
"... но это уже саааавсем другая история". В корпоративной разработке примерно в 100% случаев есть single-source-of-truth в виде gitlab\guthub\gitea\gogs - и все, что вам нужно - это добавить туда соответствующий ключ.
Проблемы тут примерно две - открытый ключ должен "принадлежать организации" - мы "доверяем ключу, пока его владелец работает в организации", а после увольнения, соответствено - уже не очень и два - закрытый ключ "по соображению безопасности" должен быть в одном (А лучше ТОЛЬКО одном) экземпляре без всяких передач туда-обратно по сети.
В идеальном мире при устройстве тебе генерируют keypair на с закрытым ключом на неизвлекаемом носителе и выдают под роспись какой-нибудь yubikey - но где ж тот идеальный мир? В реальности приходится расширять схему AD, писать открытый ключ туда и делать интеграции с gitlab'ом и, например, теми серверами куда конкретному пользователю нужен доступ.
Соответственно решать несколько задач - сами интеграции с жонглированием открытыми ключами, генерацию закрытых ключей на оконечных устройствах (Тот же openssh в windows умеет хранить закртые ключи в TPM устройства... а linux из коробки - не умеет. У маководов как всегда - своя свадьба) - ну и автоматизировать процесс записи открытого ключа в аттрибуты LDAP после генерации + описывать процесс с заменой пролюбленного\зафейленного.
В общем совсем, совсем, совсем другая история еще на две-с-половиной статьи.
alexovchinnicov
Не совсем то что ожидалось услышать от вас в контексте безопасности. Я ещё раз перечитал вашу статью и не совсем понял какую задачу вы решаете. Вот сгенерировал разработчик ssh вместо pgp ключ для подписи, загрузил его допустим GitLab SCM и указал возможность подписи им, что у вас происходит дальше кроме показа надписи что «Подписано». Проверки основываясь на ответах от публичных keyserver нет, допустим это, раз у вас разработка в корпоративном airgapped environment. Так же опустим использование ssh-agent чтобы пароль каждый раз не вводить, что тоже на мой взгляд странно. Но какая то проверка хоть в CI/CD имеется? Хочется понять для чего все это затевалось.
atshaman Автор
Ээээ... точно перечитали?
Конкретно в этой статье - туториал по настройке подписи коммитов в git в различных рабочих окружениях. Всё. Централизованного управления ключами - в этой статье нет. Complience-policy и регламента безопасной разработки (Я посмотрел!) - тоже нет. Обеспечения автоматизации следования этому регламенту я, опять же - не описывал (Но таки скажу, что в gitlab "Reject unsigned commits." - штатная настройка небесплатных редакций).
Ну и да - конечная цель в том, чтобы принимать коммиты, подписанные работниками организации - а неподписанные или подписанные не работниками - не принимать.
alexovchinnicov
Спасибо за ответ. Жаль, что понадобилось несколько комментариев с наводящими вопросами для того, что не плохо было бы поместить в саму статью.
Не задумывались сразу внедрить cosign первично для этой цели и с последующим внедрением уже проверки подписи артифактов на других этапах Secure Software Development Life Cycle (SSDLC)?
atshaman Автор
Ну, у меня было ни на чем не основанное предположение, что человек, взявшийся настраивать подпись коммитов чуть-чуть представляет зачем оно ему нужно.
Эээээто же от другой стенки гвоздь, нет? Про подпись артефактов\релизов я тоже ничего не писал ).
alexovchinnicov
Развивая свою мысль про подписание коммитов скажу, что с GitLab SCM возможно использование x509 (https://docs.gitlab.com/user/project/repository/signed_commits/x509/)
Где как мне кажется, упомянутая инфраструктура cosign будет к месту.