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

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


  1. alexovchinnicov
    05.12.2025 07:53

    Подпись это половина дела, как на счет проверки этой самой подписи? Хотелось бы увидеть как реализована эта часть, используются pgp инфраструктура keyserver?


    1. atshaman Автор
      05.12.2025 07:53

      Подпись это половина дела, как на счет проверки этой самой подписи? 

      Exactly. Но как бы ээээ... вся эта бабуйня как раз таки и затевалась, чтобы не иметь примерно "никакого" отношения к инфраструктуре PGP.

      Хотелось бы увидеть как реализована эта часть, используются pgp инфраструктура keyserver?

      "... но это уже саааавсем другая история". В корпоративной разработке примерно в 100% случаев есть single-source-of-truth в виде gitlab\guthub\gitea\gogs - и все, что вам нужно - это добавить туда соответствующий ключ.

      Проблемы тут примерно две - открытый ключ должен "принадлежать организации" - мы "доверяем ключу, пока его владелец работает в организации", а после увольнения, соответствено - уже не очень и два - закрытый ключ "по соображению безопасности" должен быть в одном (А лучше ТОЛЬКО одном) экземпляре без всяких передач туда-обратно по сети.

      В идеальном мире при устройстве тебе генерируют keypair на с закрытым ключом на неизвлекаемом носителе и выдают под роспись какой-нибудь yubikey - но где ж тот идеальный мир? В реальности приходится расширять схему AD, писать открытый ключ туда и делать интеграции с gitlab'ом и, например, теми серверами куда конкретному пользователю нужен доступ.

      Соответственно решать несколько задач - сами интеграции с жонглированием открытыми ключами, генерацию закрытых ключей на оконечных устройствах (Тот же openssh в windows умеет хранить закртые ключи в TPM устройства... а linux из коробки - не умеет. У маководов как всегда - своя свадьба) - ну и автоматизировать процесс записи открытого ключа в аттрибуты LDAP после генерации + описывать процесс с заменой пролюбленного\зафейленного.

      В общем совсем, совсем, совсем другая история еще на две-с-половиной статьи.


      1. alexovchinnicov
        05.12.2025 07:53

        Не совсем то что ожидалось услышать от вас в контексте безопасности. Я ещё раз перечитал вашу статью и не совсем понял какую задачу вы решаете. Вот сгенерировал разработчик ssh вместо pgp ключ для подписи, загрузил его допустим GitLab SCM и указал возможность подписи им, что у вас происходит дальше кроме показа надписи что «Подписано». Проверки основываясь на ответах от публичных keyserver нет, допустим это, раз у вас разработка в корпоративном airgapped environment. Так же опустим использование ssh-agent чтобы пароль каждый раз не вводить, что тоже на мой взгляд странно. Но какая то проверка хоть в CI/CD имеется? Хочется понять для чего все это затевалось.


        1. atshaman Автор
          05.12.2025 07:53

          Ээээ... точно перечитали?

          Конкретно в этой статье - туториал по настройке подписи коммитов в git в различных рабочих окружениях. Всё. Централизованного управления ключами - в этой статье нет. Complience-policy и регламента безопасной разработки (Я посмотрел!) - тоже нет. Обеспечения автоматизации следования этому регламенту я, опять же - не описывал (Но таки скажу, что в gitlab "Reject unsigned commits." - штатная настройка небесплатных редакций).

          Ну и да - конечная цель в том, чтобы принимать коммиты, подписанные работниками организации - а неподписанные или подписанные не работниками - не принимать.


          1. alexovchinnicov
            05.12.2025 07:53

            Спасибо за ответ. Жаль, что понадобилось несколько комментариев с наводящими вопросами для того, что не плохо было бы поместить в саму статью.
            Не задумывались сразу внедрить cosign первично для этой цели и с последующим внедрением уже проверки подписи артифактов на других этапах Secure Software Development Life Cycle (SSDLC)?


            1. atshaman Автор
              05.12.2025 07:53

              Спасибо за ответ. Жаль, что понадобилось несколько комментариев с наводящими вопросами для того, что не плохо было бы поместить в саму статью.

              Ну, у меня было ни на чем не основанное предположение, что человек, взявшийся настраивать подпись коммитов чуть-чуть представляет зачем оно ему нужно.

              Не задумывались сразу внедрить cosign первично для этой цели

              Эээээто же от другой стенки гвоздь, нет? Про подпись артефактов\релизов я тоже ничего не писал ).


              1. alexovchinnicov
                05.12.2025 07:53

                Развивая свою мысль про подписание коммитов скажу, что с GitLab SCM возможно использование x509 (https://docs.gitlab.com/user/project/repository/signed_commits/x509/)
                Где как мне кажется, упомянутая инфраструктура cosign будет к месту.


  1. shirmanov
    05.12.2025 07:53

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


    1. atshaman Автор
      05.12.2025 07:53

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

      Не то, чтобы я хоть капельку думал, что ФСТЕК удовлетворит подпись ключом ed25519 - но лучше иметь что-то, чем одно большое ничего, тем более что стоит оно - около "бесплатно".


  1. shirmanov
    05.12.2025 07:53

    Таких требований нет. Достаточно давно была проблема, которую эта функция решала. Также как и pgp для email. Но сейчас такой проблемы нет. Это атавизм в гите. Поэтому и не понятно какую проблему вы решаете.

    Причём тут ФСТЭК? вы им передаёте файлы целиком с контрольными суммами. Можно заменить на подписи, но требования нет. Авторство файлов всем безразлично. Это проблема юрлица заниматься отчужденим прав от разработчиков в свой адрес.

    Если вы настраиваете внутреннее окружение для разработки, соответствующее требованиям, то вам требуется контролировать пользовательские девайсы, пользовательский доступ к ним, и доступ этих пользователей к репозиториям. Подписи коммитов вам в этом никак не помогают. Не имеет никакого значения какой git user какой коммит сделал.