混沌を極めるWindowsのssh-agent事情

どうしてこうなった。

# 何の話？

WindowsでのSSH-AGENTとSSHの話です。

## この記事での用語: SSHと`ssh`, SSH-AGENTと`ssh-agent`
この記事では、SSH-AGENTと書いたときにはカテゴリとしてのSSHエージェントを意味します。
`ssh-agent`と書いたときには、実行プログラムとしての`ssh-agent`コマンドを意味します。

同様に、SSHと書いたときにはカテゴリとしてのSSHクライアントを意味します。
`ssh`と書いたときには、実行プログラムとしてのsshコマンドを意味します。


## SSH-AGENTって？

SSH-AGENTは、秘密鍵での署名を代行[^1]してくれるツールです。

[^1]: 秘密鍵を転送する、と解説されている記事もありますがそんなことはありません。秘密鍵がSSH-AGENTから出ていくことはありません。

1. SSH-AGENT に秘密鍵をロードしてしまえば、あとはパスワード(パスフレーズ)入力なしでSSH認証できる
2. agent forward機能を使うことで、SSHした先でさらにSSHするときでも、パスワード(パスフレーズ)入力なしでSSH認証できる

1は、(セキュリティのことに目をつぶると)パスフレーズなしの秘密鍵ファイルを使うことで代用できるのですが、SSH-AGENTなしで2を実現するにはリモートサーバにパスフレーズなしの秘密鍵ファイルを置いてしまうことになります。セキュリティ的にかなりオススメできません。

現代社会のインフラエンジニアに欠かせない git や ansible ではSSHを多用します。
そんなわけで、SSH-AGENTは欠かせないツールであり、agent forwardは欠かせないテクニックです。

https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-04

## SSH-AGENTとSSHの間の通信

SSHクライアントは、必要に応じてSSH-AGENTに署名を依頼することになるので、SSH-AGENT と SSHの間にはセキュアな通信チャネルが必要です。

UNIX系のOSだと、通信チャネルは Unix domain socket[^af_unix] の一択です。
[^af_unix]: 単一ホスト内でのプロセス間通信メカニズム。https://ja.wikipedia.org/wiki/UNIX%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88
しかし、Windowsでは複数のチャネルが混在しています。

* WindowsのWin32 NamedPipe[^win32_named_pipe]
[^win32_named_pipe]: Windowsプラットフォームでの単一ホスト内プロセス間通信メカニズム。https://docs.microsoft.com/ja-jp/windows/win32/ipc/named-pipes
* MSYS2 の Unix domain socket
* WSLの Unix domain socket
* WSL2 の Unix domain socket
* `pageant.exe` 互換の共有メモリ 

複数の通信チャネルと、それらを相互変換するプロキシの組み合わせにより、理解の難易度が爆発しています。

# パターン分け

## 実行形式のパターン分け

まず実行形式ファイル、つまり `.exe` ファイルのパターン分けです。
WindowsでのSSHを語る上では、実行形式に関するいくつかのグループ、種族を知らなければなりません。

### Windowsバイナリ

普通のWindowsのプログラムです。32bitか64bit。
実行形式のフォーマットは PE[^pe] (Portable Executable)です。

[^pe]: https://ja.wikipedia.org/wiki/Portable_Executable

### WSL (Windows Subsystem for Linux) バイナリ

Windows上のLinux環境であるWSL1またはWSL2で実行するプログラムです。

詳しいことはWSL1やWSL2関係の記事を読んでください。
バイナリファイル自体にWSL1とWSL2用の違いはないみたいです。
実行形式のフォーマットはELFです。

```shell
$file /bin/ls
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2f15ad836be3339dec0e2e6a3c637e08e48aacbd, for GNU/Linux 3.2.0, stripped
```

### MSYS2 バイナリ

MSYS2[^msys2] は cygwin[^cygwin] の余計な部分をそぎ落としてミニマル化したものです。
cygwin は Windows 上にUNIX的な環境を再現する、歴史あるプロジェクトです。
詳しいことはMSYS2関係の記事を読んでください。

[^msys2]: https://www.msys2.org/
[^cygwin]: https://www.cygwin.com/

2021年時点では、MSYS2はMSYS2自体での利用というよりも、Git for Windows[^GFW]のベースとして広く利用されています(主観)。Git for Windows [^GFW] は、gitのWindows移植版です。MSYS2をベースにして移植されています。以降、「GFW」と表記します。
[^GFW]: https://gitforwindows.org/

MSYS2ベースのプログラムは、MSYS2ベースのプログラムの間で通じる共通のエミュレーションレイヤを持っています。実行形式のフォーマットとしては、Windows標準のPEです。

GFWはMSYS2をベースにしていますが、本家MSYS2からはフォークしてしまっているので、GFWとMSYS2は「[混ぜるな危険](https://opcdiary.net/%E6%B7%B7%E3%81%9C%E3%82%8B%E3%81%AA%E5%8D%B1%E9%99%BA-msys2%E3%81%A8cygwin%E3%81%A8git-for-windows%E3%82%92%E4%B8%80%E7%B7%92%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%AF%E3%81%84%E3%81%91%E3%81%BE/)」だそうです。
名前は同じだけど互換性の無いDLLをロードしてしまってクラッシュする、最近はすっかり見かけなくなった[DLL地獄](https://ja.wikipedia.org/wiki/DLL%E5%9C%B0%E7%8D%84)を体験できるのかもしれません。

### その他

MSYS2の前身の MSYSとか、MinGW とか cygwin とかあるけど、ここでは考えないことにします。


## 通信チャネルの種類

次に、SSHとSSH-AGENT間の通信チャネルの分類です。

### `pageant.exe` 方式

以降、`pageant_shm` (pageant shared memory)と表記します。
Windowsで普及しているSSHクライアント、SSHスイートである putty に付属している `pageant.exe` [^pageant] の方式です。

[^pageant]: 「putty の SSH-AGENT」 だと思われますが、pagent ではありません。pageantです。英単語の pageant (ページェント、野外劇) との語呂合わせでしょうか。

http://tech.nitoyon.com/ja/blog/2008/01/07/pageant-security/

Win32のメッセージング(`WM_COPYDATA`等)や、共有メモリ機構を使っているそうです。
2015年ぐらいまで、Windows ではこの pageant 方式がSSHとSSH-AGENTの通信方式として、ほぼ唯一のものでした。(諸説あり)

### Win32 NamedPipe

以降 `NamedPipe` と表記します。
WindowsのNamedPipe機構を使う方式です。

https://docs.microsoft.com/ja-jp/windows/win32/ipc/named-pipes

`NamedPipe` の識別子の例

```
\\.\pipe\openssh-ssh-agent
```

### WSL1 Unix domain socket

以降、`UDS/WSL1` と表記します。
WSL1の世界での、Unix domain socketを使う方式です。

`UDS/WSL1` の識別子の例

```
/tmp/ssh-qe810tCXteV2/agent.16960
```

### WSL2 Unix domain socket

以降、`UDS/WSL2` と表記します。
WSL2の世界での、Unix domain socketを使う方式です。

WSL1のUnix domain socketはWSL2のUnix domain socketと互換性がありません。つまり相互通信できません。

`UDS/WSL2` の識別子の例

```
/tmp/ssh-UdWUYum9bXkJ/agent.16975
```

### MSYS2 Unix domain socket

以降、`UDS/MSYS2` と表記します。
MSYS2の枠組みでの、Unix domain socketを使う方式です。

`UDS/MSYS2` の識別子の例

```
/c/tmp/ssh-UdWUYum9bXkJ/agent.16975
```

## WindowsのSSH実装

実行形式のパターン分けと、通信チャネルのパターン分けが分かったところで、WindowsのSSHクライアントの実装を見てみます。

### Win32-OpenSSH

Microsoftによる、OpenSSHの移植版です。64bit版もWin32-OpenSSHです。64ビット版のファイル名は `OpenSSH-Win64.zip` になっています。
https://github.com/PowerShell/Win32-OpenSSH

SSH-AGENTとの通信は `NamedPipe` です。

### pageant 互換のSSHクライアント

putty[^putty], RLogin[^rlogin], TeraTerm[^teraterm], FileZilla[^filezilla], WinSCP[^filezilla] などです。
SSH-AGENTとの通信は `pageant_shm` です。

[^putty]: https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html
[^rlogin]: http://nanno.dip.jp/softlib/man/rlogin/
[^teraterm]: https://ja.osdn.net/projects/ttssh2/
[^filezilla]: https://filezilla-project.org/
[^winscp]: https://winscp.net/eng/index.php

### GFW内蔵SSH

Git for Windowsに同梱されるSSHです。GFWがMSYS2ベースなので、GFWに内蔵されるSSHもMSYS2ベースです。
SSH-AGENTとの通信は `UDS/MSYS2` です。

### WSL SSH

WSL1/2でインストールされる SSH です。
他のディストリビューションでも違いはないと思いますが、ストアでインストールできるUbuntu 20.04 LTSの前提で書いています。

WSL1環境で実行する場合、SSH-AGENTとの通信は `UDS/WSL1` です。
WSL2環境で実行する場合、SSH-AGENTとの通信は `UDS/WSL2` です。

## WindowsのSSH-AGENT実装

次にSSH-AGENTの実装です。

### Win32-OpenSSH の `ssh-agent.exe`

Win32-OpenSSHに同梱される `ssh-agent.exe` です。
SSHとの通信は `NamedPipe` です。

### Git For Windows の `ssh-agent.exe`

Git for Windows に同梱される `ssh-agent.exe` です。
SSHとの通信は `UDS/MSYS2` です。
Win32-OpenSSHの`ssh-agent.exe`とは同じ名前ですが、動作は別物です。

### putty の `pageant.exe`

puttyに同梱される本家 `pageant.exe` です。
SSHとの通信は `pageant_shm` です。

嬉しいことに、バージョン 0.77 から、`NamedPipe` がサポートされました。

https://the.earth.li/~sgtatham/putty/0.77/htmldoc/Chapter9.html#pageant-cmdline

> Windows's own port of OpenSSH uses the same mechanism as Pageant to talk to its SSH agent (Windows named pipes).

* `pageant.exe` もGUIのウィンドウと、実体としてのSSH-AGENT("its SSH agent") は`NamedPipe`で通信している。
* そのSSH-AGENTと通信するためのNamed pipeを、OpenSSHから使えるようにするよ。

ということのようです。詳しくは[後述](#本家`pageant.exe`のOpenSSH連携機能)します。



### `pageant.exe` 互換SSH-AGENT

`pageant.exe` の互換 SSH-AGENTです。

本家 `pageant.exe` が `NamedPipe` もサポートするので、`pageant.exe` 互換 SSH-AGENTを使う意味は無いと思います。

<details><summary>古い内容。一応文章は残しておきます。</summary>

`pageant.exe` 互換 SSH-AGENT である `pageant+` [^pageant+] は、複数の通信チャネルに対応しています。

[^pageant+]: バイナリは`pageant+.exe` だが、リポジトリは `pageant-`。

https://github.com/zmatsuo/pageant-

* `UDS/MSYS2`
    * pageant+ のサイトの表記だと、Git for Windows, OpenSSH(msys2)
* `UDS/WSL1`
    * pageant+ のサイトの表記だと、OpenSSH(WSL)
* cygwin方式
    * pageant+ のサイトの表記だと、OpenSSH(Cygwin)
* `NamedPipe`
    * pageant+ のサイトの表記だと、OpenSSH(Microsoft版)
* `pageant_shm`
    * 当然ながら、普通の pageant.exe と同じ方式も動作します。

マルチプロトコルに対応しているという点で貴重なSSH-AGENTなのですが、2021年3月時点では残念ながら開発が停止しているようです。
動作が不安定なところがあります。

* KeePassXC 等から、APIで秘密鍵を追加するときにクラッシュすることがある。
* ansibleで複数台に処理するときのように、多数のリクエストが同時に発生すると、取りこぼす時がある。

</details>

### WSL1/WSL2 の `ssh-agent`

WSLのopenssh に同梱される `ssh-agent` です。

## SSH-AGENTプロキシ

基本的には、SSHクライアントは同族のSSH-AGENTとしか通信できません。

例) WSL2の`ssh`は、WSL2の`ssh-agent` としか通信できない。

しかし、それだと使うクライアントの種類の数だけSSH-AGENTが必要になってしまいます。
もともとの、「SSH秘密鍵が必要になる署名処理を1か所にまとめる」という利便性がスポイルされます。
SSHクライアントが同族のSSH-AGENTとしか通信できない、という問題を解決するために、SSH-AGENTプロキシが開発されています。

異なる通信チャネル間をプロキシ接続することができます。

例) WSL2の`ssh`(`UDS/WSL2`) と `pageant.exe`(`pageant_shm`) の間を接続することで、WSL2の`ssh` から `pageant.exe`にロードした秘密鍵を使用できる。

### wsl-ssh-ageant

https://github.com/rupor-github/wsl-ssh-agent

* 実行形式: Windowsバイナリ
* 受け入れるクライアント
  * クライアントとの通信チャネル: `UDS/WSL1`
  * クライアントの例: WSL1 の SSH
* 接続先SSH-AGENT
  * SSH-AGENTとの通信チャネル: `NamedPipe`
  * SSH-AGENTの例: OpenSSH-Win32 の SSH-AGENT

名前がややこしいですが、ssh-wsl-agent[^ssh-agent-wsl] は、このwsl-ssh-agent の前身です。忘れましょう。

[^ssh-agent-wsl]: https://github.com/rupor-github/ssh-agent-wsl

### wsl-ssh-pageant

https://github.com/benpye/wsl-ssh-pageant

* 実行形式: Windowsバイナリ
* 受け入れるクライアント1
  * クライアントとの通信チャネル: `NamedPipe`
  * クライアントの例: OpenSSH-Win32 の SSH
* 受け入れるクライアント2
  * クライアントとの通信チャネル: `UDS/WSL1`
  * クライアントの例: WSL1 の SSH
* 接続先SSH-AGENT
  * SSH-AGENTとの通信チャネル: `pageant_shm`
  * SSH-AGENTの例: pageant.exe

OpenSSH-Win32 と WSL1 の2つタイプのクライアントと接続できる優れものです。

### wsl2-ssh-pageant

https://github.com/BlackReloaded/wsl2-ssh-pageant

* 実行形式: Windowsバイナリ
* 受け入れるクライアント
  * クライアントとの通信チャネル: `UDS/WSL2`
      - ただし `socat` で中継する必要あり
  * クライアントの例: WSL2 の SSH
* 接続先SSH-AGENT
  * SSH-AGENTとの通信チャネル: `pageant_shm`
  * SSH-AGENTの例: pageant.exe

wsl-ssh-pageant の WSL2 対応版です。
SSHクライアントとの接続には、`socat` を使います。

wsl2-ssh-pageantのサイトから[引用](https://github.com/BlackReloaded/wsl2-ssh-pageant#bashzsh)。

```bash
export SSH_AUTH_SOCK=$HOME/.ssh/agent.sock
ss -a | grep -q $SSH_AUTH_SOCK
if [ $? -ne 0 ]; then
        rm -f $SSH_AUTH_SOCK
        (setsid nohup socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:$HOME/.ssh/wsl2-ssh-pageant.exe >/dev/null 2>&1 &)
fi
```

### ssh-pageant

https://github.com/cuviper/ssh-pageant

* 実行形式: MSYS2バイナリ
* 受け入れるクライアント
  * クライアントとの通信チャネル: `UDS/MSYS2`
  * クライアントの例: GFW同梱の SSH
* 接続先SSH-AGENT
  * SSH-AGENTとの通信チャネル: `pageant_shm`
  * SSH-AGENTの例: pageant.exe


コードリポジトリは[前掲のgithub](https://github.com/cuviper/ssh-pageant) のようですが、GFWにバイナリが同梱されています。
個別にダウンロードする必要はありません。

### socat

`socat` は2つのストリームをリレー接続する汎用ツールです。SSH, SSH-AGENT専用ではありません。
wsl2-ssh-pageant の例では、UNIX domain socket と、EXECで起動したプログラム(wsl2-ssh-pageant.exe)の標準入出力をリレーしています。

この記事では、WSL の SSH とwsl2-ssh-pageant 等の `socat` を使いますが、`socat`自体はUNIX OS汎用のツールです。

https://linux.die.net/man/1/socat


### npiperelay

https://github.com/jstarks/npiperelay

npiperelay は、Win32 NamedPipe と標準入出力をリレーする汎用ツールです。SSH, SSH-AGENT専用ではありません。
`socat` と組み合わせることで、Unix domain socket と、`NamedPipe` をリレー接続することができます。

github の npiperelay でもバイナリが配布されていますが、wsl-ssh-agent の配布物にもバイナリが同梱されています。

### 本家`pageant.exe`のOpenSSH連携機能

`pageant.exe` はバージョン 0.77 から、`NamedPipe` をサポートしています。

具体的には、コマンドラインに `--openssh-config` を指定して起動することで、OpenSSH (Win32-OpenSSH) 用のコンフィグが生成されます。

```powershell
PS> pageant --openssh-config pageant.conf
PS> Get-Content pageant.conf
IdentityAgent "//./pipe/pageant.username.1dd0bd6d8626a420747bc803b28..."
```

`pageant.exe`を終了させるとファイルの中身は空になります。
`.ssh/config` から `Include` するのが良いです。

```powershell
PS> pageant --openssh-config $Env:USERPROFILE\pageant.conf
PS> Get-Content $Env:USERPROFILE\.ssh\config
# pageant.exe が生成するコンフィグを読み込む
Include pageant.conf
PS>
```

## キーストア

最後にキーストアです。SSH秘密鍵を保持しておく場所です。

大きく2つあります。

* 秘密鍵ファイル
* パスワードマネージャ

他に、Windows OSの証明書ストアや、スマートカード/セキュリティトークン、Gnu PG Keyringをキーストアとして利用できるSSHクライアントもあるそうです。
詳しくは[@ttdodaさんのコメント](https://qiita.com/slotport/items/e1d5a5dbd3aa7c6a2a24#comment-175c9054e3eb9670ab62) をどうぞ。


### 秘密鍵ファイル

ファイルシステム上のファイルとして、存在するSSH秘密鍵です。要はふつうのファイルです。

SSH秘密鍵にパスフレーズ[^passphrase]を設定している場合、秘密鍵をロードするときにパスフレーズの入力が必要です。
秘密鍵ファイルが3つあると、3回入力することになるので面倒です。

パスフレーズ無しにしておくことで入力の手間が省けます。
パスフレーズ無しの秘密鍵ファイルというと、セキュリティ警察がやってくる場合がありますが、秘密鍵ファイルは信頼するパソコンにしか置かないとするのであれば、利便性を取ってパスフレーズ無しにしておくのもありです。

ただし、パスフレーズ無しの秘密鍵ファイルというのは、平文のパスワードと同じ意味であることは知っておくべきです。

[^passphrase]: パスワードと同じ意味です。「パスフレーズ」と書くときにはパスワード(pass 'word')よりも長い、15文字以上といったフレーズ(pass `phrase`)を意図することが多いです。でも、本人しか知らない秘密の合言葉という意味では、パスワードもパスフレーズも同じです。


### パスワードマネージャ

Webサイト等のログインアカウントを管理するパスワードマネージャです。
パスワードマネージャの中には、SSH秘密鍵を管理する機能を持つものもあります。

#### KeePassXC

KeePassXCもSSH秘密鍵管理機能を持つパスワードマネージャの1つです。
https://keepassxc.org/

エントリの添付ファイルとしてSSH秘密鍵を添付しておくと、KeePassXCのデータベースを開いたタイミングで、SSH-AGENTに秘密鍵を追加してくれます。

https://keepassxc.org/docs/#faq-ssh-agent-how

>Q: How does the SSH Agent work?
>A: The SSH Agent feature is supported on all target platforms (Linux, macOS and Windows) and it acts as a client for an existing agent. It can automatically add SSH keys from your KeePassXC database to a running SSH agent when unlocked and remove them when locked.

追加する先のSSH-AGENTとして、pageant (`pageant_shm`方式) のほかに、Win32 OpenSSH (`NamedPipe`方式)も選択できます。
片方、または両方に対して追加することができます[^16]。

[^16]: Version 2.6.6 以前は pageant (`pageant_shm`方式) と、Win32 OpenSSH (`NamedPipe`方式) との二者択一でしたが、2022/5/22 のVersion 2.7.0 から両方に対して秘密鍵を追加できるようになりました。

パスワードマネージャのマスターキーでデータベースを開いたタイミングで、SSH-AGENTへSSH秘密鍵を追加してくれます。

#### KeePass

KeePass は KeePassXC のフォーク元のパスワードマネージャです[^15]。
https://keepass.info/

[^15]: 正確にはKeePassXCのフォーク元のKeePassXのさらにフォーク元です。

KeePass単体ではSSH関連機能は持ちませんが、プラグインのKeeAgentを入れることで、pageant 互換の SSH-AGENT として動作します。
https://lechnology.com/software/keeagent/

KeePassXCのSSH連携機能は、SSH-AGENTへキーを追加する機能であって、SSH-AGENTそのものではないですが、KeeAgentはSSH-AGENTそのものになります。`pageant_shm` 方式です。

# まとめチャート

以上をまとめたチャートが下図です。

![window-ssh-agent-20230930.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107657/f08d2c60-6f59-8a0b-5b04-fe97af33fe1a.png)

## 筆者の構成

筆者の構成は以下の通りです。

![window-ssh-agent-Ver3.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107657/5aec222d-52cf-f55a-23d9-2a9b791e5f06.png)

* SSH秘密鍵はKeePassXCで管理する。
* putty 標準の `pageant.exe` を以下のコマンドラインで常駐させておく。
  * `--openssh-config $Env:USERPROFILE\.ssh\config.d\pageant.conf`
  * タスクスケジューラから起動する場合は環境変数の記法が変わる。
    * `--openssh-config %USERPROFILE%\.ssh\config.d\pageant.conf`
* Win32-OpenSSHのコンフィグファイル `$Env:USERPROFILE\.ssh\config` から `pageant.conf` を `Include` する。
* WSL2 の `.bash_profile` で、wsl2-ssh-pageant.exe + socat を起動する。
  * https://github.com/BlackReloaded/wsl2-ssh-pageant の設定どおり。

`pageant.exe` が `NamedPipe` をサポートしたので、`wsl-ssh-pageant.exe` が不要になり、ずいぶんすっきりしました。
これで、以下3種に対して秘密鍵認証を提供できます。

* Win32 OpenSSH
* PuTTY互換クライアント
* WSL2 SSH