Привет, Хабр! Я Алексей Шаманов, старший системный архитектор в ГК “Цифра”, занимаюсь проектированием решений на базе платформы 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 есть аж целых три способа:

  1. Переустановить git с использованием внешнего OpenSSH:

External ssh
External ssh

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 с ключом заработает!

Правда fine...
Правда fine...

И в общем всё в этом способе хорошо, но очень уж развесистая гирлянда выходит. Да и использование 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 и т. д. не рассматриваем как очевидно маргинальные) и можно задуматься и о централизованном управлении ключами в корпоративной среде - но это уже са-аа-авсем другая история...

Комментарии (0)