← シリーズに戻る

ssh -vv で読む接続の5段階を実測

Part 1 / 6 SSH接続の5方式を7軸比較(4方式実測)

ssh が繋がらないとき、最初に決めたいのは鍵の問題なのかネットワークの問題なのかです。この2つは対処が正反対で、取り違えると何時間も溶けます。

SSH で「鍵」と呼ばれるものは3つあります。ログインに使う自分の鍵 (ユーザー鍵。秘密鍵と公開鍵の対)、サーバーが本物であることを示すホスト鍵、通信を暗号化する使い捨ての鍵を両者で作る鍵交換です。ここで言う「鍵の問題」は1つ目のユーザー鍵の話です。

SSH で「鍵」と呼ばれる3つを並べた図。ユーザー鍵 (自分の秘密鍵と公開鍵の対) はログインに使い自分が誰かを証明する、使われるのは5 ユーザー認証の段階、ssh -vv の目印は Authentications that can continue:。「鍵の問題」はこれで、5段階目より前では出番が無い。ホスト鍵 (サーバーが持つ) はサーバーが本物であることを示す、4 鍵交換とホスト鍵検証の段階、目印は Server host key:。鍵交換 (両者でその場で作る使い捨ての鍵) は通信を暗号化する、4 鍵交換とホスト鍵検証の段階、目印は kex: algorithm:。MTU 1280 の境界を超えうるのはこのパケット (5章)。下に「4段階目で止まったら、ユーザー鍵より経路を疑う。鍵交換のパケットが大きさで落ちることがある」

見分け方は単純で、ssh -vv の出力が5段階のどこで止まったかを見ます。4段階目までに止まっていたら、自分の秘密鍵はまだ一度も使われていません。 つまりユーザー鍵は無関係です。

OpenSSH 10.0p2 のクライアントから同じ 10.0p2 のサーバーに入ったときの実際のログを、段階ごとに切って読みます。

段階ごとに何が起きているか

debug1: Connecting to lab-target [172.19.0.2] port 22.
debug1: Local version string SSH-2.0-OpenSSH_10.0p2 Debian-7+deb13u4
debug1: Remote protocol version 2.0, remote software version OpenSSH_10.0p2 Debian-7+deb13u4
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: algorithm: mlkem768x25519-sha256
debug1: kex: host key algorithm: ssh-ed25519
debug1: Server host key: ssh-ed25519 SHA256:3kstJR8PODEpAfBSzXcvsukN+wNfIZtHhG55W0a7o84
debug1: Authentications that can continue: publickey,password

この9行が5段階です。

段階目印になる行やっていること
1 TCP接続Connecting to ... port 22名前解決してTCPを張る
2 バージョン交換Local version string / Remote protocol version平文でお互いの実装と版を名乗る
3 アルゴリズム交渉SSH2_MSG_KEXINIT sent / received使える暗号方式の一覧を送り合う
4 鍵交換とホスト鍵検証kex: algorithm: / Server host key:共通鍵を作り、相手が本物かを確かめる
5 ユーザー認証Authentications that can continue:ここではじめて鍵やパスワードを出す

クライアントとサーバーのあいだの5段階を上から順に並べたシーケンス図。1 TCP接続 (名前解決して TCP を張る)、2 バージョン交換 (実装と版を名乗り、サーバーの版は nc でも読める)、3 アルゴリズム交渉 (使える暗号方式の一覧 KEXINIT を送り合う)、4 鍵交換とホスト鍵検証 (鍵交換パケット ECDH_INIT を送り、ホスト鍵がここで届く)、5 ユーザー認証 (ここで初めて自分の秘密鍵を使う)。1〜2段階は平文の帯で囲まれている。右端に各段階の ssh -vv の目印の行を並べ、下に「4段階目までに止まっていたら、自分の秘密鍵はまだ一度も使われていない」と書いてある

(電話にたとえると、1〜4段階はつながって名乗り合い、相手が本人かを確かめるところまでで、自分が誰かを証明する (秘密鍵を使う) のは5段階目です)

2段階目までは平文です。 バージョン文字列はまだ暗号化されていないので、nc でポートを叩くだけでも読めます。

$ head -c 40 < /dev/tcp/127.0.0.1/2222
SSH-2.0-OpenSSH_10.0p2 Debian-7+deb13u4

サーバーのOpenSSHの版がここで分かることは、Tailscale越しのSSHとMTU 1280の章で効いてきます。交渉される鍵交換方式は両端の版で決まるからです。

止まった位置から原因を引く

最終行段階疑う先
Connecting to ... のまま1経路・ファイアウォール・そもそもsshdが動いていない
Remote protocol version まで2稀。sshd が接続直後に落ちている
SSH2_MSG_KEXINIT sent まで3送信方向のパケットが届いていない
expecting SSH2_MSG_KEX_ECDH_REPLY4ユーザー鍵より経路を疑う。 パケットサイズとMTU (1個のIPパケットで運べる最大の大きさ) を見る
Permission denied (publickey)5ここではじめて鍵とユーザー名の話になる

止まった位置から疑う先を引く図。1〜5段階を横に並べ、1〜4段階の上に「自分の秘密鍵はまだ使われていない」、5段階の上に「鍵の出番」と帯を掛け、4と5のあいだに線を引いている。最終行と疑う先は、1 Connecting to ... のまま → 経路・ファイアウォール・sshd が動いていない、2 Remote protocol version まで → 稀。sshd が接続直後に落ちている、3 SSH2_MSG_KEXINIT sent まで → 送信方向のパケットが届いていない、4 expecting SSH2_MSG_KEX_ECDH_REPLY → パケットサイズと MTU を見る (強調)、5 Permission denied (publickey) → ここではじめて鍵とユーザー名の話。下に「5段階目に到達していないログで、自分の鍵を疑わない」

4段階目で止まる形は実際に踏みました。post-quantum な鍵交換 (量子計算機でも解けないとされる暗号を組み込んだ鍵交換) では、クライアントが公開鍵を載せて送るパケットが 48 バイトから 1232 バイトへ膨らみ、MTU 1280 の経路で TCP が1回に運べる上限 (1228 バイト) を 4 バイト超えます。その経路で接続が止まりました。超えれば必ず止まるとは限らず、どういう条件で止まりうるかはTailscale越しのSSHとMTU 1280の章で、止まった経緯はWSL2 Tailscale SSHがKEXで停止に書きました。

5段階目の括弧を読む

認証まで到達したら、拒否メッセージの括弧の中を見ます。サーバーが何を受け付ける気があるかが書いてあります。

deploy@lab-target: Permission denied (publickey,password).

括弧の中は、サーバーがこの先受け付ける認証方式の一覧です。ここが (publickey) だけなら、パスワードという選択肢自体が提示されていません。入力したパスワードの正誤とは関係がありません。 同じ Permission denied でも原因が違うので、括弧の中身まで読みます。サーバー側でパスワード認証を止めた前後で括弧がどう変わるかは、SSHの鍵認証でパスワードを消す4手順の章で実測しました。

手元で同じログを出す

コンテナ2つで再現できます。2〜4章の実測も、主にこのラボ (4章では踏み台を足して3台) で取りました。5章は実際の tailnet で測り、6章は実行していません。

# sshd を持つイメージを作る
cat > Dockerfile <<'DOCKER'
FROM debian:trixie-slim
RUN apt-get update -qq && apt-get install -y -qq openssh-server rsync \
 && rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash deploy && echo 'deploy:hunter2' | chpasswd \
 && mkdir -p /run/sshd && ssh-keygen -A
CMD ["/usr/sbin/sshd","-D","-e"]
DOCKER
docker build -t sshlab .

docker network create sshlabnet
docker run -d --name lab-target --network sshlabnet sshlab

クライアント側もコンテナにすると、手元の ~/.ssh を汚さずに試せます。

docker run -d --name lab-client --network sshlabnet debian:trixie-slim sleep infinity
docker exec lab-client sh -c 'apt-get update -qq && apt-get install -y -qq openssh-client sshpass rsync'
docker exec lab-client ssh -vv -o StrictHostKeyChecking=no deploy@lab-target true

このシリーズのラボ構成の図。docker network sshlabnet の中に2台のコンテナがある。左の lab-client はイメージ debian で、openssh-client・sshpass・rsync を入れ、sleep infinity で起こしておく。手元の ~/.ssh を汚さずに試せる。右の lab-target は Dockerfile で作るイメージ sshlab で、openssh-server と rsync、ユーザー deploy、ssh-keygen -A でホスト鍵を作り、sshd -D -e で前面に起動する。lab-client から lab-target の22番へ ssh -vv deploy@lab-target true の矢印。下に「両端とも OpenSSH 10.0p2。この章のログも、2〜4章の主な実測もこのラボで取った。4章では踏み台 (lab-bastion) を足して3台にし、送信先をクライアントから届かない場所に移す」

この章の結論

ssh -vv の最終行を5段階のどれかに置く。それだけで、自分の鍵を見るべきか経路を見るべきかが決まります。次の章では5段階目に進んで、パスワードを鍵に置き換えます。