WSL2 から 1Password の SSH エージェントを使ってローカルの秘密鍵ファイルをなくす

WSL2 から 1Password の SSH エージェントを使ってローカルの秘密鍵ファイルをなくす

WSL2で1PasswordのSSHエージェントを使うため、npiperelay.exeとsocatでWindowsと Linux間を中継する設定に挑戦しました。過去の失敗を乗り越え、秘密鍵ファイルの削除とGitHub PAT の廃止に成功した記録です。
2026.08.17

コーヒーが好きな emi です。

端末に平文でクレデンシャルを置かない構成にするため、Windows 11 上の WSL2 で使っている Git の認証を 1Password の SSH エージェントに寄せました。

弊社は 1Password を導入しています。1Password には SSH エージェント機能があり、保管庫に入れた鍵をそのまま ssh に使わせられます。秘密鍵をディスク上のファイルとして持たずに済み、GitHub へのアクセスも PAT(Personal Access Token)なしで通せます。

この構成は過去に一度試してうまく動かず、途中でやめていた作業です。今回あらためて手をつけて最後まで通せたので、その記録を残します。

今回扱うのは SSH 鍵です。AWS のアクセスキーは、JUMP アカウント分をすでに AWS Vault で扱っているので、この記事では触れません。
AWS のアクセスキーを 1Password にのせる手順は以下を参照ください。1Password CLI と op plugin を使って、WSL2 上で AWS の一時クレデンシャルを 1Password 経由で取得する方法を解説しています。
https://dev.classmethod.jp/articles/202602-1password-cli-aws-credential-on-wsl2/

なお、1Password を使えない環境でどうするかは別途考える必要があり、本記事では扱いません。

動作環境

項目 バージョン
Windows Windows 11 Enterprise 10.0.26200
ディストリビューション Ubuntu 22.04.5 LTS(Jammy Jellyfish)
OpenSSH 8.9p1 Ubuntu-3ubuntu0.13
socat 1.7.4.1
npiperelay v0.1.0
1Password 8.12.32.33

1Password への SSH 鍵の登録はすでに済んでいる前提で進めます。

1Password の SSH エージェントとは

SSH エージェントは、秘密鍵を預かっておいて、認証で署名が必要になったときだけ代理で署名を返すプログラムです。鍵そのものを ssh コマンドに渡さないところがポイントで、ssh は「このデータに署名してほしい」と頼み、エージェントが署名だけを返します。鍵はエージェントの内側から出てきません。

1Password の SSH エージェントは、この役目を 1Password の保管庫に入れた鍵で果たしてくれます。ディスク上に秘密鍵ファイルを置く必要がなくなり、鍵を使うたびに 1Password の承認が挟まります。

Windows 版の 1Password では、設定画面の「開発者」から SSH エージェントを有効にします。

wsl2-1password-ssh-agent_5
wsl2-1password-ssh-agent_6
wsl2-1password-ssh-agent_7

ここで 1 つ前提があります。Windows には、もともと「OpenSSH 認証エージェント」というサービスが入っています。これも SSH エージェントなので、動いていると 1Password が SSH エージェントの役目を引き継げません。同じ役目を 2 つのプログラムが同時に担うことはできないためです。

このサービスは既定では無効になっています。私の環境でも無効のままだったので、特に操作は要りませんでした。動いている場合の止め方は 1Password の公式ドキュメントに載っています。

https://www.1password.dev/ssh/get-started

なぜ WSL2 から素直に使えないのか

1Password は Windows 側で動いています。一方、鍵を使いたい ssh コマンドは WSL2 という Linux の中にいます。同じパソコンの中にいるのに、住んでいる世界が違います。

プログラム同士が会話するには、話しかけるための口が要ります。この口の作りが Windows と Linux で違います。

Windows の 1Password が開けている口は名前付きパイプというもので、\\.\pipe\openssh-ssh-agent という名前が付いています。対して WSL2 の ssh が話しかけられる口は Unix ドメインソケットです。こちらはファイルのパスが待ち合わせ場所になっていて、sshSSH_AUTH_SOCK という環境変数で「この場所にいる相手と話して」と教えられます。

Unix ドメインソケットとは

同じマシンの中でプログラム同士がデータをやりとりするための仕組みです。ネットワーク越しの通信では IP アドレスとポート番号で相手を指定しますが、Unix ドメインソケットではファイルのパスで指定します。マシンの外には出ません。

パスで指定するので ls で見えます。この記事で作る ~/.ssh/agent.sock はこう見えます。

emiki@<hostname>:~$ ls -l ~/.ssh/agent.sock
srwxr-xr-x 1 emiki emiki 0 Aug 17 09:18 /home/emiki/.ssh/agent.sock

行頭が s になっているのが、これがソケットである印です。普通のファイルなら -、ディレクトリなら d が出るところです。サイズが 0 なのは、中身を溜めておくファイルではなく、待ち合わせ場所として名前だけが存在しているからです。片方のプログラムがこの場所で待ち受け、もう片方がここへ話しかけにきます。

やりたいことはどちらも「データを行き来させる」ことなのに、差込口の形が違うので、そのままでは挿さりません。

そこで、間に通訳を 2 つ立てます。1 つで足りないのは、それぞれの通訳が片側の言葉しか分からないからです。

  • npiperelay.exe
    • 名前付きパイプと標準入出力の間を通訳する(Windows 側で動く)
  • socat
    • 標準入出力と Unix ドメインソケットの間を通訳する(Linux 側で動く)

標準入出力は、どちらの世界にもある共通語のようなものです。2 つの通訳がこの共通語で手をつなぎ、端から端まで繋がります。

このように、間に入ってデータを流し続ける仕組みをリレーと呼びます。npiperelay という名前の relay もこの意味です。この記事でも以降はこの言い方をします。

npiperelay.exe

Windows 側で動く実行ファイルです。自分の標準入力に来たデータを名前付きパイプへ流し、パイプから返ってきたデータを自分の標準出力へ出します。つまり、名前付きパイプを「ただの標準入出力」として扱えるようにしてくれる道具です。

作者の jstarks 氏は Microsoft で WSL や Hyper-V を手がけているエンジニアです。

https://github.com/jstarks/npiperelay

socat

ソックキャットまたはソーキャットと読みます。Linux 側のコマンドです。2 つの入出力口を指定すると、その間でデータを右から左へ流し続けてくれます。名前は SOcket CAT の略で、cat の相手をソケットなどに広げたもの、と考えると掴みやすいと思います。

https://dev.classmethod.jp/articles/relay-tcp-with-socat/

今回は「Unix ドメインソケットを作って待ち受ける口」と「npiperelay.exe を起動してその標準入出力と話す口」の 2 つを繋ぎます。WSL2 からは /mnt/c/... 経由で Windows の実行ファイルをそのまま起動できるので、socat から npiperelay.exe を呼べます。

Ubuntu なら apt で入ります。

sudo apt install socat

ssh から見れば、話し相手は ~/.ssh/agent.sock にいる普通の SSH エージェントです。その先で socat と npiperelay.exe がリレーしていることは意識しません。

npiperelay.exe を置く

GitHub のリリースから取得します。バージョンを決め打ちせず、releases API で最新を確認します。

curl -sSL https://api.github.com/repos/jstarks/npiperelay/releases/latest \
  | grep -E '"(tag_name|browser_download_url)"'
実行結果例
emiki@<hostname>:~$ curl -sSL https://api.github.com/repos/jstarks/npiperelay/releases/latest \
>   | grep -E '"(tag_name|browser_download_url)"'
  "tag_name": "v0.1.0",
      "browser_download_url": "https://github.com/jstarks/npiperelay/releases/download/v0.1.0/npiperelay_0.1.0_checksums.txt"
      "browser_download_url": "https://github.com/jstarks/npiperelay/releases/download/v0.1.0/npiperelay_windows_386.zip"
      "browser_download_url": "https://github.com/jstarks/npiperelay/releases/download/v0.1.0/npiperelay_windows_amd64.zip"

最新は v0.1.0 で、2020 年 7 月の公開でした。それ以降リリースはありません。

zip とチェックサムファイルをダウンロードして照合します。

チェックサムとは

ファイルの中身から計算した短い文字列です。中身が 1 バイトでも違えば、まったく別の文字列になります。ファイルの指紋のようなものだと思ってください。今回の計算方法は SHA-256 で、コマンド名も sha256sum になっています。

npiperelay の作者は、リリースした zip のチェックサムを npiperelay_0.1.0_checksums.txt として一緒に公開しています。手元の zip から同じ計算をして一致すれば、途中で壊れたり中身が入れ替わったりしていないと分かります。ただしチェックサムも同じリリースページに置かれているので、リリースそのものが差し替えられた場合までは防げません。

この先の実行結果に出てくる SHA256:VnGnegUS... も同じ考え方です。こちらは公開鍵から計算したもので、SSH では fingerprint と呼びます。同じ SHA-256 ですが、チェックサムは 16 進数、fingerprint は Base64 なので見た目が違います。

curl -sSLO https://github.com/jstarks/npiperelay/releases/download/v0.1.0/npiperelay_windows_amd64.zip
curl -sSLO https://github.com/jstarks/npiperelay/releases/download/v0.1.0/npiperelay_0.1.0_checksums.txt
cat npiperelay_0.1.0_checksums.txt
sha256sum --check --ignore-missing npiperelay_0.1.0_checksums.txt
実行結果例
emiki@<hostname>:~$ curl -sSLO https://github.com/jstarks/npiperelay/releases/download/v0.1.0/npiperelay_windows_amd64.zip
emiki@<hostname>:~$ curl -sSLO https://github.com/jstarks/npiperelay/releases/download/v0.1.0/npiperelay_0.1.0_checksums.txt
emiki@<hostname>:~$ cat npiperelay_0.1.0_checksums.txt
6b9ef61ffd17c03507a9a3d54d815dceb3dae669ac67fc3bf4225d1e764ce5f6  npiperelay_windows_amd64.zip
600e3f94f0d9f48e68b5a9276849d00d658ae9c18c81541463ed11b1dfe6c998  npiperelay_windows_386.zip

emiki@<hostname>:~$ sha256sum --check --ignore-missing npiperelay_0.1.0_checksums.txt
npiperelay_windows_amd64.zip: OK

チェックサムファイルには 2 行あります。npiperelay_windows_amd64.zip が 64 bit 版で、npiperelay_windows_386.zip が 32 bit 版です。386 は Intel 80386 という古い CPU に由来する呼び方で、32 bit の Windows 向けを指します。

今回ダウンロードしたのは 64 bit 版だけです。--ignore-missing を付けないと、sha256sum が手元に無い 32 bit 版まで探しに行って失敗します。

--ignore-missing を付けずに実行した場合
emiki@<hostname>:~$ sha256sum --check npiperelay_0.1.0_checksums.txt; echo "exit=$?"
npiperelay_windows_amd64.zip: OK
sha256sum: npiperelay_windows_386.zip: No such file or directory
npiperelay_windows_386.zip: FAILED open or read
sha256sum: WARNING: 1 listed file could not be read
exit=1

照合が取れたら展開して、Windows 側に置きます。Windows の実行ファイルなので、WSL 側ではなく Windows のユーザーフォルダ配下に置きます。

unzip -o npiperelay_windows_amd64.zip
mkdir -p /mnt/c/Users/emiki/bin
cp npiperelay.exe /mnt/c/Users/emiki/bin/npiperelay.exe
ls -l /mnt/c/Users/emiki/bin/
実行結果例
emiki@<hostname>:~$ unzip -o npiperelay_windows_amd64.zip
Archive:  npiperelay_windows_amd64.zip
  inflating: LICENSE
  inflating: README.md
  inflating: npiperelay.exe

emiki@<hostname>:~$ mkdir -p /mnt/c/Users/emiki/bin
emiki@<hostname>:~$ cp npiperelay.exe /mnt/c/Users/emiki/bin/npiperelay.exe
emiki@<hostname>:~$ ls -l /mnt/c/Users/emiki/bin/
total 1832
-rwxrwxrwx 1 emiki emiki 1872384 Aug 17 09:18 npiperelay.exe
余談 置き場所に迷った話

過去に試したときは、npiperelay.exe をどこに置くかで手が止まりました。参考にしていた記事には「パスの通った場所」にコピーするとあり、著者の方は WSL 側の /usr/local/bin を選んでいます。

https://qiita.com/mfunaki/items/db6e1ffcf1e6f1eff252

/usr/local/bin でも動きます。ただ、そこは Linux のバイナリを置く場所なので、Windows の実行ファイルを混ぜるのが自分にはしっくり来ませんでした。WSL からは /mnt/c/... 経由で Windows の実行ファイルをそのまま起動できるので、今回は Windows 側のディレクトリに置いて、そこを socat から参照しています。

socat でリレーを張る

socat で Unix ドメインソケットを作り、その先を npiperelay.exe に繋いで、WSL2 から 1Password まで一本の道を通します。

このリレーは WSL を再起動すると消えるので、最終的には起動処理を .bashrc に書いて、シェルを開くたびに自動で張り直すようにします。

ただ、いきなり .bashrc に書いてしまうと、シェルを開くたびに裏で黙って走るだけになります。うまく動いていなくても画面には何も出ないので、後の手順でつまずいたときに、原因がリレーなのか設定なのか分からなくなります。

そこで、まずは手で 1 回動かして、ssh-add -l で 1Password の鍵が見えるところまで確かめます。

export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
rm -f "$SSH_AUTH_SOCK"
( setsid socat UNIX-LISTEN:"$SSH_AUTH_SOCK",fork \
    EXEC:"/mnt/c/Users/emiki/bin/npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork & ) \
    >/dev/null 2>&1
コマンドの解説

1 行目の export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock" は、ssh にエージェントの居場所を教える指定です。この後 socat が作るソケットも、ここで決めた場所に作ります。

2 行目の rm -f "$SSH_AUTH_SOCK" は、前に作ったソケットファイルの片付けです。同じパスにファイルが残っていると socat は新しく作れません。消さずに起動しようとすると、こうなります。

emiki@<hostname>:~$ socat UNIX-LISTEN:leftover.sock,fork EXEC:"/bin/cat",nofork
2026/08/17 15:01:26 socat[71167] E "leftover.sock" exists

3 行目からが本体です。まわりを囲んでいる setsid& と丸かっこは、socat のオプションではなくシェルの書き方です。

  • 末尾の &
    • 裏で動かしてすぐプロンプトを返す指定。これだけだと socat がそのターミナルにぶら下がったままになり、ターミナルを閉じると一緒に終了してリレーが切れる
  • setsid
    • プロセスをターミナルから切り離して独立させる。閉じても socat は動き続けるので、別のターミナルを開いてもそのまま ssh が使える
  • 全体を囲む ( ... )
    • このシェルのジョブとして扱わせないため。囲まないと [1] 12345 のようなジョブ番号や終了通知が出てくる

socat 自身に渡しているのは、繋ぐ 2 つの口の指定です。

  • UNIX-LISTEN:...,fork
    • Unix ドメインソケットを作って待ち受ける側。fork を付けると接続のたびに子プロセスを作るので、複数のターミナルから同時に使える
  • EXEC:"...",nofork
    • 相手側。指定したコマンドを起動して、その標準入出力と繋ぐ
  • EXEC で起動している npiperelay.exe-ei は入力が EOF になったら閉じる指定、-s は書き込み側を先に閉じる指定

最後の >/dev/null 2>&1 は出力を捨てる指定です。手で試している段階では、ここを外しておくと socat のエラーが画面に出るので原因を追いやすくなります。

余談 ソケットのパスを間違えて socat が起動しなかった

過去に試したときは、SSH_AUTH_SOCK~/.1password/agent.sock を指定していました。参照した手順にそう書かれていたのをそのまま写したのですが、~/.1password というディレクトリが手元に無く、socat が待ち受けを作れませんでした。

emiki@<hostname>:~/work$ export SSH_AUTH_SOCK=$HOME/.1password/agent.sock
emiki@<hostname>:~/work$ socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:"...",nofork &
[1] 10758
2025/11/28 12:04:04 socat[10758] E bind(5, {AF=1 "/home/emiki/.1password/agent.sock"}, 35): No such file or directory

socat は親ディレクトリを作ってはくれません。すでにある ~/.ssh の下にソケットを作るようにしたら通りました。

動いたか確認します。

ls -l "$SSH_AUTH_SOCK"
ssh-add -l
実行結果例
emiki@<hostname>:~$ ls -l "$SSH_AUTH_SOCK"
srwxr-xr-x 1 emiki emiki 0 Aug 17 09:18 /home/emiki/.ssh/agent.sock

emiki@<hostname>:~$ ssh-add -l
3072 SHA256:VnGnegUSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx my-ssh-key (RSA)

WSL2 側から 1Password の鍵が見えました!!!!!!!!!!

確認できたので .bashrc に書きます。WSL は再起動するとプロセスが消えるため、シェルを開くたびにリレーを張り直す必要があります。

# 1Password SSH agent (Windows) を WSL から使う
export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
if ! ss -a 2>/dev/null | grep -q "$SSH_AUTH_SOCK"; then
  rm -f "$SSH_AUTH_SOCK"
  ( setsid socat UNIX-LISTEN:"$SSH_AUTH_SOCK",fork \
      EXEC:"/mnt/c/Users/emiki/bin/npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork & ) \
      >/dev/null 2>&1
fi

ss -a でソケットがすでに待ち受けているかを見て、あれば起動をスキップします。これがないと、ターミナルを開くたびに socat が増えていきます。

二重起動していないか確認しておきます。

ss -a | grep agent.sock
pgrep -a -x socat
実行結果例
emiki@<hostname>:~$ ss -a | grep agent.sock
u_str LISTEN  0      5                 /home/emiki/.ssh/agent.sock 73087                    * 0

emiki@<hostname>:~$ pgrep -a -x socat
55857 socat UNIX-LISTEN:/home/emiki/.ssh/agent.sock,fork EXEC:/mnt/c/Users/emiki/bin/npiperelay.exe -ei -s //./pipe/openssh-ssh-agent,nofork

1 本だけです。

なお、ここで pgrep -c -f "socat UNIX-LISTEN" のように -f で探すと 2 と出ます。pgrep を呼んだシェル自身のコマンドラインが検索パターンにマッチしてしまうためで、socat が 2 本あるわけではありません。-x でプロセス名そのものを見るほうが確実です。

~/.ssh/config を直す

私の ~/.ssh/config は、GitHub と Backlog の 2 ブロックで鍵ファイルを直接指定していました。両方ともエージェント経由に切り替えます。

  • IdentityAgent ~/.ssh/agent.sock を追加して、エージェントの居場所を教える
  • IdentityFile ~/.ssh/my-ssh-key を削除する
  • IdentitiesOnly yes を削除する

3 つ目が今回いちばんのハマりどころでした。IdentitiesOnly yes は「設定ファイルで明示した鍵だけを使う」という指定です。これが残っていると、エージェントが差し出してくる鍵を ssh が無視してしまいます。IdentityFile を消しただけで満足していると、認証が通らずに悩むことになります。

私が過去にあきらめたのもこの行が理由でした。ssh-add -l では 1Password の鍵が見えているのに、ssh -T git@github.comPermission denied (publickey) で弾かれ続け、原因に辿り着けないまま時間切れになりました。

変更後の ~/.ssh/config はこうなりました。

~/.ssh/config
### my-ssh-key 設定

#### GitHub の設定
# - - - - - - - - - - - -
Host github.com
  User git
  Port 22
  Hostname github.com
  PreferredAuthentications publickey
  IdentityAgent ~/.ssh/agent.sock #秘密鍵は 1Password の SSH エージェントを使うようにする
  TCPKeepAlive yes #ここは好み

#### Backlog の設定
# - - - - - - - - - - - -
Host example.git.backlog.jp
  User example
  Port 22
  Hostname example.git.backlog.jp
  PreferredAuthentications publickey
  IdentityAgent ~/.ssh/agent.sock #秘密鍵は 1Password の SSH エージェントを使うようにする
  TCPKeepAlive yes #ここは好み

疎通確認

GitHub と Backlog に繋ぎます。-T は端末を割り当てない指定で、シェルを開かずに認証だけ試したいときに使います。終了コードも一緒に見ておきます。

ssh -T git@github.com; echo "exit=$?"
ssh -T example@example.git.backlog.jp; echo "exit=$?"
実行結果例
emiki@<hostname>:~$ ssh -T git@github.com; echo "exit=$?"
Hi emi-ki! You've successfully authenticated, but GitHub does not provide shell access.
exit=1

emiki@<hostname>:~$ ssh -T example@example.git.backlog.jp; echo "exit=$?"
You've successfully authenticated, but Backlog does not provide shell access.
exit=1

どちらも認証に成功しています。

終了コードが exit=1 と出ていますが、これは失敗ではありません。認証が通ったかどうかは、Hi emi-ki! のようなメッセージのほうで判断します。

終了コードと、ここで 1 が返る理由

コマンドは終わるときに、うまくいったかどうかを数字でシェルに返します。これを終了コードと言います。0 が成功で、それ以外は何か問題があったことを表します。

普段は画面に出ないので意識しませんが、直前のコマンドの終了コードは $? という変数に入っています。echo "exit=$?" を付けているのは、これを見るためです。

emiki@<hostname>:~$ ls /etc/hostname; echo "exit=$?"
/etc/hostname
exit=0

emiki@<hostname>:~$ ls /etc/nothing; echo "exit=$?"
ls: cannot access '/etc/nothing': No such file or directory
exit=2

存在するファイルを指定したときは 0、存在しないファイルのときは 2 が返っています。シェルスクリプトや CI は、この数字を見て次に進むかどうかを判断します。

その決まりでいくと 1 は「何か問題があった」を表す数字です。ですが今回の 1 は、ssh が認証に失敗したという意味ではありません。そう言える根拠は ssh のマニュアルにあります。

ssh exits with the exit status of the remote command or with 255 if an error occurred.

https://man.openbsd.org/ssh

ssh が返す数字は、接続先で動いたプログラムの終了コードをそのまま持ち帰ったものです。ssh 自身が失敗したときだけ 255 になります。試しに存在しないホストへ繋ごうとすると、255 が返ります。

emiki@<hostname>:~$ ssh -o ConnectTimeout=3 nonexistent.invalid true; echo "exit=$?"
ssh: Could not resolve hostname nonexistent.invalid: Name or service not known
exit=255

今回返ってきたのは 255 ではなく 1 でした。ssh はここまでの仕事を終えていて、1 は接続先が決めた数字だと分かります。GitHub も Backlog も、Git のやりとりのためだけに SSH を開けています。ログインしてきた相手にシェルは使わせず、あいさつのメッセージを返したら 1 で接続を切る、という応答をしています。

余談 GitHub 側から公開鍵が消えていた

過去に試したときは、そもそも GitHub に登録したはずの公開鍵が無くなっていました。GitHub は 1 年間使われなかった SSH キーを自動的に削除します。

https://docs.github.com/ja/authentication/troubleshooting-ssh/deleted-or-missing-ssh-keys

Permission denied (publickey) が出たとき、原因が手元の設定とは限りません。GitHub の Settings から SSH and GPG keys を開いて、鍵が登録されたままかどうかも見てみてください。私はここで登録し直しました。

使われた鍵がエージェント由来になっているかどうかを見ておきます。

ssh -v -T git@github.com 2>&1 | grep -iE "Offering|Server accepts|Authenticated to"
実行結果例
emiki@<hostname>:~$ ssh -v -T git@github.com 2>&1 | grep -iE "Offering|Server accepts|Authenticated to"
debug1: Offering public key: my-ssh-key RSA SHA256:VnGnegUSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx agent
debug1: Server accepts key: my-ssh-key RSA SHA256:VnGnegUSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx agent
Authenticated to github.com ([20.27.177.113]:22) using "publickey".

行末に agent と付いています。ファイルから読んだ鍵ならここにファイルパスが出るので、エージェントから渡された鍵で認証したことがわかります。

エージェント経由で初めて鍵を使うとき、Windows 側に 1Password の承認ダイアログが出ます。

wsl2-1password-ssh-agent_1

ここで承認すると ssh が先に進み、Hi emi-ki! が返ってきます。

一度承認すれば、1Password がロックまたは終了するまで聞かれなくなります。

Any subsequent SSH commands run in that process can use your key without further approval until 1Password locks or quits, or for the amount of time set in the options you've configured

https://www.1password.dev/ssh/agent/security

ロックされると秘密鍵はエージェントのメモリから外れるので、翌朝など間が空いたときは 1Password のロック解除を求められます。

CI やスクリプトから Git を動かしていて、待ち時間に上限を設けている場合は、この承認やロック解除の待ちを見込んでおくとよさそうです。

remote を SSH に寄せる

鍵の経路ができたので、次は Git の接続先を HTTPS から SSH に変更します。HTTPS のままだと PAT が使われるためです。

~/.gitconfig に 1 行足します。

git config --global url."git@github.com:".insteadOf "https://github.com/"

これで https://github.com/ で始まる URL を Git が自動的に git@github.com: に読み替えます。すでに手元にあるリポジトリだけでなく、HTTPS の URL を直接指定したときも同じように読み替わります。こちらは記事の後半で確かめます。

ここで足したのは読み替えのルールだけです。各リポジトリの設定ファイルである .git/config には手を触れていないので、そちらには HTTPS の URL が残ったままになります。

実行結果例
emiki@<hostname>:~/work/slide-tool$ grep -m1 "url = " .git/config
	url = https://github.com/example-org/slide-tool.git

emiki@<hostname>:~/work/slide-tool$ git remote -v
origin	git@github.com:example-org/slide-tool.git (fetch)
origin	git@github.com:example-org/slide-tool.git (push)

emiki@<hostname>:~/work/slide-tool$ git ls-remote --get-url origin
git@github.com:example-org/slide-tool.git

同じリポジトリを見ているのに、.git/config は HTTPS、git remote -v は SSH と、表示が食い違っています。git remote -v のほうは読み替えたあとの姿を見せているためです。通信は SSH で行われるので、動きとしてはこれで目的を果たしています。

ただ、この状態は ~/.gitconfig に足した 1 行に頼っています。その行が消えたり、別のマシンにリポジトリを持っていったりすると HTTPS に戻ります。そこで、リポジトリ側の URL も書き換えておきます。

git -C ~/work/workshop-repo remote set-url origin git@github.com:example-org/workshop-repo.git
git -C ~/work/handson-repo remote set-url origin git@github.com:example-org/handson-repo.git
git -C ~/work/slide-tool remote set-url origin git@github.com:example-org/slide-tool.git

それぞれ git fetch が通ることを確認します。

実行結果例
emiki@<hostname>:~$ for d in workshop-repo handson-repo slide-tool; do
>   printf '%-16s ' "$d"; git -C ~/work/$d fetch >/dev/null 2>&1; echo "exit=$?"
> done
workshop-repo    exit=0
handson-repo     exit=0
slide-tool       exit=0

ローカルの鍵ファイルを消す

ここまでの疎通が全部通ってから消します。

rm -v ~/.ssh/my-ssh-key ~/.ssh/my-ssh-key:Zone.Identifier
ls -la ~/.ssh/
実行結果例
emiki@<hostname>:~$ rm -v ~/.ssh/my-ssh-key ~/.ssh/my-ssh-key:Zone.Identifier
removed '/home/emiki/.ssh/my-ssh-key'
removed '/home/emiki/.ssh/my-ssh-key:Zone.Identifier'

emiki@<hostname>:~$ ls -la ~/.ssh/
total 28
drwx------  2 emiki emiki 4096 Aug 17 09:32 .
drwxr-x--- 24 emiki emiki 4096 Aug 17 09:29 ..
srwxr-xr-x  1 emiki emiki    0 Aug 17 09:18 agent.sock
-rw-r--r--  1 emiki emiki  575 Mar 26  2023 my-ssh-key.pub
-rw-r--r--  1 emiki emiki  617 Aug 17 09:24 config
-rw-------  1 emiki emiki 1342 Oct 28  2025 known_hosts
-rw-------  1 emiki emiki 1120 Oct 28  2025 known_hosts.old

my-ssh-key:Zone.Identifier は、Windows からコピーしたファイルに付いてくる副産物です。公開鍵の my-ssh-key.pub は秘密情報ではないので残しました。

`:Zone.Identifier` とは何なのか

中身を見ると、25 バイトのテキストです。

emiki@<hostname>:~$ cat "WSL2.png:Zone.Identifier"
[ZoneTransfer]
ZoneId=3

ZoneId=3 は「インターネットから取得した」という印です。Windows はこれを見て、「別のコンピューターから取得したファイルです」という警告や SmartScreen のブロックを出します。

もともとは NTFS の代替データストリームという、ファイルに付いた隠しポケットのような場所に入っています。Windows 上では独立したファイルには見えません。ところが WSL 側の ext4 にはそういう仕組みがないので、エクスプローラーで \\wsl.localhost にコピーすると、中身が普通のファイルとして書き出されてしまいます。これが目につく :Zone.Identifier の正体です。

Linux 側はこのファイルを読まないので、WSL の中だけで使うぶんには消して困ることはありません。今回の my-ssh-key:Zone.Identifier も、鍵ファイルと一緒に消してしまって差し支えないものです。

ただ、これはただのゴミファイルではありません。Windows から \\wsl.localhost 経由で同じファイルを見ると、今でも印として読み取れます。

Get-Content -Path '\\wsl.localhost\Ubuntu\home\emiki\work\blog\WSL2.png' -Stream Zone.Identifier
[ZoneTransfer]
ZoneId=3

WSL 側でこのファイルを消すと、Windows から見たときの印も消えます。素性の分からない実行ファイルについては、残しておいたほうが安全です。印が付いていれば、Windows から実行しようとしたときに警告や SmartScreen のブロックが働いて、うっかり実行してしまうのを止めてくれます。

消したあと、もう一度疎通を確認します。

実行結果例
emiki@<hostname>:~$ ssh -T git@github.com
Hi emi-ki! You've successfully authenticated, but GitHub does not provide shell access.

emiki@<hostname>:~$ for d in workshop-repo handson-repo slide-tool company-match-quiz legacy-repo; do
>   printf '%-20s ' "$d"; git -C ~/work/$d fetch >/dev/null 2>&1; echo "exit=$?"
> done
workshop-repo        exit=0
handson-repo         exit=0
slide-tool           exit=0
company-match-quiz   exit=0
legacy-repo          exit=128

legacy-repo だけ失敗していますが、これは過去に作業していたリポジトリで参照権限がないものなので、想定通りです。

おまけ PAT と pass の後片付け

ここからはおまけです。SSH に寄せたことで GitHub の PAT を使う場面が無くなったので、ついでに片付けます。

GitHub の Settings > Developer Settings > Personal access tokens と遷移し、fine-grained token を Delete します。
wsl2-1password-ssh-agent_2

消えました。
wsl2-1password-ssh-agent_3

Tokens (classic) のタブにも残っていないことを確認しました。
wsl2-1password-ssh-agent_4

続いて、PAT を保管していた pass のエントリを消します。順序を逆にすると、Git Credential Manager が PAT を再入力しにきて紛らわしいので、GitHub 側を先に消してからにします。

pass rm -r -f git/https/github.com
pass ls
実行結果例
emiki@<hostname>:~$ pass rm -r -f git/https/github.com
removed '/home/emiki/.password-store/git/https/github.com/example-org/slide-tool.git/emi-ki.gpg'
removed directory '/home/emiki/.password-store/git/https/github.com/example-org/slide-tool.git'
removed '/home/emiki/.password-store/git/https/github.com/example-org/cc-plugin.git/emi-ki.gpg'
removed directory '/home/emiki/.password-store/git/https/github.com/example-org/cc-plugin.git'
removed '/home/emiki/.password-store/git/https/github.com/example-org/handson-repo/emi-ki.gpg'
removed directory '/home/emiki/.password-store/git/https/github.com/example-org/handson-repo'
removed directory '/home/emiki/.password-store/git/https/github.com/example-org'
removed '/home/emiki/.password-store/git/https/github.com/emi-ki/company-match-quiz.git/86865300.gpg'
removed directory '/home/emiki/.password-store/git/https/github.com/emi-ki/company-match-quiz.git'
removed directory '/home/emiki/.password-store/git/https/github.com/emi-ki'
removed directory '/home/emiki/.password-store/git/https/github.com/'

emiki@<hostname>:~$ pass ls
Password Store
├── claude-code
   └── grafana-otlp-token
├── default
├── git
   └── https
└── sts.GetSessionToken,...

github.com 配下だけが消えて、無関係なエントリは残りました。

最後に ~/.gitconfig から GitHub 向けの credential 設定を消します。

git config --global --remove-section 'credential.https://github.com'

Git Credential Manager 本体と、GitHub 以外のホスト向けの設定はそのまま残します。

ここまで済んだ状態で、HTTPS の URL を直接叩いてみます。

GIT_SSH_COMMAND="ssh -v" git ls-remote https://github.com/example-org/slide-tool.git 2>&1 \
  | grep -iE "Offering|Authenticated to"
実行結果例
emiki@<hostname>:~$ GIT_SSH_COMMAND="ssh -v" git ls-remote https://github.com/example-org/slide-tool.git 2>&1 | grep -iE "Offering|Authenticated to"
debug1: Offering public key: my-ssh-key RSA SHA256:VnGnegUSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx agent
Authenticated to github.com ([20.27.177.113]:22) using "publickey".

PAT がどこにもない状態で、HTTPS の URL が SSH に読み替えられ、1Password の鍵で認証されました。

おまけ WSL を再起動してもリレーは張り直されるか

.bashrc に書いたので張り直されるはずですが、実際に確かめておきます。socat を落として、新しいシェルを開きます。

pkill -x socat
pgrep -a -x socat || echo "socat なし"
ls -l ~/.ssh/agent.sock
実行結果例
emiki@<hostname>:~$ pkill -x socat
emiki@<hostname>:~$ pgrep -a -x socat || echo "socat なし"
socat なし
emiki@<hostname>:~$ ls -l ~/.ssh/agent.sock
ls: cannot access '/home/emiki/.ssh/agent.sock': No such file or directory

socat は終了するときにソケットファイルも片付けてくれました。.bashrc に書いた rm -f は、socat が異常終了してファイルだけ残った場合の保険という位置づけになります。

この状態で新しいシェルを開きます。

実行結果例
emiki@<hostname>:~$ bash -ic 'echo "SSH_AUTH_SOCK=$SSH_AUTH_SOCK"; ssh-add -l'
SSH_AUTH_SOCK=/home/emiki/.ssh/agent.sock
3072 SHA256:VnGnegUSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx my-ssh-key (RSA)

emiki@<hostname>:~$ pgrep -a -x socat
57203 socat UNIX-LISTEN:/home/emiki/.ssh/agent.sock,fork EXEC:/mnt/c/Users/emiki/bin/npiperelay.exe -ei -s //./pipe/openssh-ssh-agent,nofork

張り直されました。

1Password 移行できた部分と、これから対応したいこと

WSL2 で Claude Code を使うようになってから、認証まわりで何度か転んでいます。今回の構成で、そのうちいくつかは前提ごとなくなりました。

GitHub の PAT を更新するたびに、GCM と GPG と pinentry を順に通す必要がありました。PAT 自体を消したので、この作業がもう発生しません。

https://dev.classmethod.jp/articles/wsl-pass-github-pat-pinentry-timeout/

Claude Code のスキルを使えるようにするまでに転んだときも、つまずきの多くは GCM と GPG と pass を重ねた認証構成と、Fine-grained PAT のスコープ管理が原因でした。SSH に寄せた今は、PAT のスコープを気にする場面がありません。

https://dev.classmethod.jp/articles/wsl-claude-code-skills-struggle/

Claude Code の画面が崩れる問題にも遭っていました。こちらは pinentry を GUI 版に切り替えて解決済みです。

https://dev.classmethod.jp/articles/wsl-garbled-text-fix/

ただし、GPG のパスフレーズを入力する場面そのものは残っています。OTLP 認証トークンを pass に置いていて、取り出すたびに GPG を通るからです。今回 SSH を 1Password に寄せても、こちらには手が届いていません。

https://dev.classmethod.jp/articles/claude-code-otlp-token-pass-wsl-grafana-cloud/

このトークンも 1Password に寄せられれば、パスフレーズの入力ごとなくなりそうです。これから試してみます。

おわりに

WSL2 の ~/.ssh から秘密鍵ファイルが消え、GitHub の PAT もなくなりました。Git の操作は 1Password の承認だけで通るようになっています。

npiperelay.exe を置いて socat でリレーを張り、~/.ssh/config を書き換える、という流れでした。一度あきらめた作業だったので、通って嬉しかったです。

1Password を使えない環境では、この手は使えません。その場合は pass と GPG で暗号化して持つ構成になると思うので、先ほど挙げた過去記事のほうが参考になるはずです。

本記事への質問やご要望については画面下部の「DevelopersIOへのご意見」からお問い合わせいただけます。

参考

https://github.com/jstarks/npiperelay

https://www.1password.dev/ssh/agent

https://www.1password.dev/ssh/integrations/wsl

http://www.dest-unreach.org/socat/doc/socat.html

https://man.openbsd.org/ssh_config#IdentityAgent

https://dev.classmethod.jp/articles/save-ssh-private-key-in-1password/

https://dev.classmethod.jp/articles/1password-git-ssh/

https://dev.classmethod.jp/articles/202602-1password-cli-aws-credential-on-wsl2/

https://dev.classmethod.jp/articles/devcontainer-1password-auth/

この記事をシェアする

関連記事