
SSH の仕組み - 鍵交換・ホスト鍵検証・公開鍵認証を一次ソースで理解する
Linux とコマンド操作の定番入門
TCP/IP の全体像を押さえる一冊
認証と暗号の実務理解に役立つ
当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。
サーバーにログインするとき、ファイルを転送するとき、Git で push するとき。その裏で当たり前のように使われているのが SSH(Secure Shell)です。パスワードを入力するか、あるいは鍵ファイルさえあれば、離れた場所のマシンに安全につながる。その「安全に」の中身を、なんとなくの理解のまま使っている人は少なくありません。
この記事では、SSH がどのように接続を確立し、通信相手を確かめ、ユーザーを認証しているのかを、一次ソースである RFC 4251 / 4252 / 4253 / 4254(SSH-2 の Architecture / Authentication / Transport / Connection の4本)を軸に整理します。鍵交換、ホスト鍵の検証、公開鍵認証、そしてポートフォワーディングまで、OpenSSH での確認方法も添えて一通り押さえます。
NOTE
本記事は手順・用語・アルゴリズム名を RFC 4251〜4254 などの一次ソースと OpenSSH の公式 man ページを参照して整理しています。実装や設定による差異、確定できない箇所については「未確認」と明示します。
SSH とは何か(まず全体像)
SSH は、信頼できないネットワークの上で安全なリモートログインとコマンド実行、その他のネットワークサービスを提供するためのプロトコルです。RFC 4251(The Secure Shell (SSH) Protocol Architecture)は、SSH-2 を次の3つの層で構成すると定義しています。
- トランスポート層プロトコル(RFC 4253): サーバー認証、機密性(暗号化)、完全性を提供する。通常は TCP/IP の上で動く
- ユーザー認証プロトコル(RFC 4252): トランスポート層の上で、クライアント側のユーザーをサーバーに対して認証する
- 接続プロトコル(RFC 4254): 暗号化されたトンネルを複数の論理チャネルに多重化し、対話シェルやポートフォワーディングを実現する
つまり SSH は、「まず安全な土管を作る(トランスポート層)」「その中で誰がログインするかを確かめる(認証層)」「その上で複数の用途を同時に流す(接続層)」という三段構えになっています。この分担を頭に入れておくと、以降の話が整理しやすくなります。
なお、既定の待ち受けポートは TCP 22番で、IANA が SSH に割り当てています(RFC 4253)。かつての SSH-1(プロトコルバージョン 1.x)は設計上の弱点があり、現在は使われません。本記事は現行の SSH-2 を対象とします。
接続確立の流れ(TCP から認証まで)
まず、クライアントがサーバーにつながってからログインが済むまでの全体の流れをシーケンス図で見ます。
一番下で TCP がつながる部分は、TCP と UDP の 3ウェイハンドシェイクそのものです。SSH はその上に構築されるアプリケーション層のプロトコルだと捉えると位置づけが分かりやすくなります。
バージョン交換とアルゴリズムのネゴシエーション
TCP がつながると、双方はまず識別文字列(identification string)を送り合います。RFC 4253 が定める形式は SSH-protoversion-softwareversion SP comments CR LF で、SSH-2 では protoversion が 2.0 になります。実際には SSH-2.0-OpenSSH_10.4 のような文字列が流れます。この文字列は最大255文字(CR と LF を含む)と定められています。
続いて双方が SSH_MSG_KEXINIT を送り、鍵交換・ホスト鍵・暗号・MAC・圧縮それぞれについて、対応アルゴリズムの名前リスト(name-list)を提示します。各リストの先頭が最優先で、両者の候補を突き合わせて実際に使う組み合わせを決めます。ここで「サーバーとクライアントが対応アルゴリズムを一つも共有していない」と接続は成立しません。古い機器につなげないときに、まずこのネゴシエーションを疑うのはそのためです。
RFC 4253 が相互運用のために必須としている鍵交換方式は diffie-hellman-group1-sha1 と diffie-hellman-group14-sha1 で、いずれも SHA-1 を使う古いものです。現在の OpenSSH は、より新しい curve25519-sha256(Curve25519 を用いた鍵交換、RFC 8731 で標準化)などを既定で優先します。SHA-1 ベースの旧方式は安全性の観点から無効化が進んでいます。
ホスト鍵の検証(known_hosts と TOFU)
鍵交換の過程で、サーバーは自分のホスト鍵(host key)で署名を行い、クライアントは「つないだ相手が確かに本物のサーバーか」を確かめます。これがなければ、経路上の攻撃者がサーバーになりすます中間者攻撃(MITM)を防げません。この「公開鍵で署名を検証し、秘密鍵の所有を確かめる」構図は、公開鍵暗号と電子署名の考え方そのものです。ホスト鍵のアルゴリズムには、RFC 4253 時点では ssh-dss(必須)や ssh-rsa(推奨)が定義され、現在は ssh-ed25519(Ed25519、RFC 8709 で標準化)が広く使われています。
問題は「初めてつなぐサーバーの鍵が本物かをどう知るか」です。RFC 4251 は、ホスト鍵の信頼モデルとして2つを挙げています。
| モデル | 仕組み | 特徴 |
|---|---|---|
| ローカルデータベース | ホスト名と公開ホスト鍵の対応をクライアント側に保存 | 中央インフラ不要だが、鍵の管理・更新は自分で行う |
| 認証局(CA) | ホスト名と鍵の対応を信頼された CA が証明 | 管理は楽だが、中央インフラへの依存が生じる |
OpenSSH で普段使われるのはローカルデータベース方式で、その実体が ~/.ssh/known_hosts です。初めて接続するホストでは、次のようにフィンガープリントの確認を求められます。
The authenticity of host 'example.com (203.0.113.10)' can't be established.
ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?ここで yes と答えると、その鍵が known_hosts に保存され、次回以降は自動で照合されます。この「初回だけは相手を信じて、以降はその鍵を覚えておく」方式は TOFU(Trust On First Use)と呼ばれます。
RFC 4251 は、この初回接続の扱いを明確な弱点として認めています。初回に鍵検証を省けることは移行期の利便性を高めますが、その一回が MITM に晒されると攻撃者の鍵を覚え込んでしまいます。だからこそ、初回のフィンガープリントは別経路(構築ログや管理コンソール)で照合するのが望ましいです。なお、2回目以降でフィンガープリントが変わっていた場合、OpenSSH は大きな警告を出して接続を止めます。これはサーバー再構築で鍵が変わっただけのこともありますが、なりすましの可能性も含めて必ず原因を確認すべきサインです。
WARNING
「鍵が変わった」という警告が出たときに、理由を確かめずに known_hosts の該当行を消して再接続するのは危険です。正当なサーバー鍵の変更なのか、経路上のなりすましなのかを、必ず別の手段で確認してから対応してください。
公開鍵認証の仕組み(authorized_keys と署名)
土管が安全になり相手も確かめられたら、次はユーザーの認証です。RFC 4252(The Secure Shell (SSH) Authentication Protocol)は認証方式として publickey(必須)、password、hostbased、none を定義しています。実務で推奨されるのは 公開鍵認証(publickey)です。
公開鍵認証は、大きく2段階で進みます。
- 問い合わせ: クライアントが「この公開鍵で認証できますか」と、署名なし(フラグを偽)で打診する
- 認証: 受理されそうなら、クライアントは署名付き(フラグを真)で認証要求を送る
肝心なのが2段階目の署名の中身です。RFC 4252 によれば、クライアントはセッション識別子(最初の鍵交換で導出される値)に加え、メッセージ種別・ユーザー名・サービス名・方式名・鍵アルゴリズム・公開鍵そのものを、この順に並べたデータへ秘密鍵で署名します。サーバーはその署名を公開鍵で検証します。
ここでのポイントは、秘密鍵はネットワークに一切流れないことです。流れるのは署名だけで、しかも署名対象にはコネクション固有のセッション識別子が含まれるため、盗んだ署名を別の接続で使い回すこともできません。パスワード認証(password)はパケット全体が暗号化されるとはいえ、パスワードの推測・使い回し・漏洩のリスクが残ります。公開鍵認証がインフラ運用で標準とされる理由がここにあります。
サーバー側は、ログインを許可する公開鍵を ~/.ssh/authorized_keys に列挙して管理します。鍵ペアは ssh-keygen で作成します。OpenSSH の現行 man ページによれば、ssh-keygen を引数なしで実行すると Ed25519 の鍵が生成され、-t で指定できる鍵種別は ecdsa / ecdsa-sk / ed25519 / ed25519-sk / mldsa44-ed25519 / rsa です(DSA は一覧から外れています)。
# Ed25519 の鍵ペアを作成(コメントを付けておくと後で識別しやすい)
ssh-keygen -t ed25519 -C "you@example.com"
# 公開鍵をサーバーの authorized_keys に登録する
ssh-copy-id user@example.comssh-keygen は秘密鍵(例: ~/.ssh/id_ed25519)と、末尾に .pub が付いた公開鍵(~/.ssh/id_ed25519.pub)を生成します。サーバーに渡すのは .pub の公開鍵だけで、秘密鍵は手元から出しません。秘密鍵にはパスフレーズを付け、後述の ssh-agent と組み合わせると、毎回の入力を省きつつ鍵ファイル単体の漏洩リスクを下げられます。
接続の多重化とポートフォワーディング
認証が済むと、いよいよ接続プロトコル(RFC 4254)の出番です。RFC 4254 は「すべての端末セッション、フォワードされた接続などはチャネルである」と述べています。1本の暗号化されたコネクションの中に複数のチャネルを多重化し、それぞれをウィンドウサイズで流量制御しながら並行して扱います。チャネルは SSH_MSG_CHANNEL_OPEN で開き、SSH_MSG_CHANNEL_DATA でデータを流し、SSH_MSG_CHANNEL_CLOSE で閉じます。だから1つの SSH 接続で、シェル操作をしながら同時にポート転送を走らせる、といったことができます。
そのポートフォワーディングを OpenSSH のオプションで整理すると次のようになります。
| オプション | 名前 | 向き | 用途の例 |
|---|---|---|---|
-L | ローカルフォワード | 手元の待受ポート -> リモート経由で目的地へ | 手元から社内 DB にトンネル経由で接続 |
-R | リモートフォワード | サーバーの待受ポート -> 手元経由で目的地へ | 手元の開発サーバーを外部へ一時公開 |
-D | ダイナミックフォワード | 手元に SOCKS プロキシを立てる | 通信をまとめて SSH 経由に流す |
具体的なコマンド例です。実務での確認ポイントも添えます。
# ローカルフォワード: 手元の 15432 を、サーバー経由で db.internal:5432 へ
ssh -L 15432:db.internal:5432 user@bastion.example.com
# リモートフォワード: サーバーの 8080 を、手元の localhost:3000 へ
ssh -R 8080:localhost:3000 user@example.com
# ダイナミックフォワード: 手元の 1080 に SOCKS プロキシを立てる
ssh -D 1080 user@example.com-L の書式は [bind_address:]port:host:hostport で、「手元の port への接続を、サーバー側から見た host:hostport へ転送する」と読みます。踏み台(bastion)越しに内部ネットワークのサービスへ届くのは、目的地の名前解決と接続がサーバー側で行われるからです。-R はその逆向きで、手元のサービスをサーバー側の待受ポートに露出させます。外部公開になり得るため、-R を使うときは待ち受けアドレスと公開範囲に注意が必要です。
実務での設定と確認
接続先が増えてくると、毎回長いオプションを打つのは現実的ではありません。~/.ssh/config にホストごとの設定をまとめておくと、ssh myserver の一行でつながります。
Host myserver
HostName example.com
User deploy
Port 22
IdentityFile ~/.ssh/id_ed25519Port の既定値は22なので、標準ポートなら省略できます。別ポートで待ち受けているサーバーには、コマンドラインなら ssh -p 2222 user@host のように -p で指定します(-L などの数値ではなく接続先ポートの指定です)。
鍵の扱いを楽にするのが ssh-agent です。パスフレーズ付きの秘密鍵を一度 ssh-add で登録しておけば、以降はエージェントが署名を代行し、毎回の入力が不要になります。エージェントはメモリ上で鍵を扱い、秘密鍵ファイル自体を各所へばら撒かずに済む点が利点です。
うまくつながらないときは、詳細ログが手掛かりになります。
# 認証や鍵交換のどこで止まっているかを詳細表示する
ssh -v user@example.com
# さらに詳しく(-vvv まで指定できる)
ssh -vvv user@example.com-v の出力には、ネゴシエーションで選ばれた鍵交換・ホスト鍵・暗号のアルゴリズム、どの鍵ファイルで認証を試みたか、known_hosts の照合結果などが順に並びます。「どの段階で失敗したか」を切り分ける最初の一歩として有効です。
サーバーを運用する側では、SSH の設定を締めることが基本的なセキュリティ対策になります。具体的には、パスワード認証を無効化して公開鍵認証のみにする、root の直接ログインを禁止する、といった設定です。最近の OpenSSH の変更点は OpenSSH 10.4 の新機能とセキュリティ修正でも扱っています。なお、SSH の上でファイル転送を行う SFTP を lftp から使う手順は lftp で SFTP サーバーに接続する方法にまとめています。
まとめ
- SSH-2 はトランスポート層(RFC 4253)・認証層(RFC 4252)・接続層(RFC 4254)の三層構成。既定ポートは TCP 22番
- 接続は「TCP 3ウェイハンドシェイク -> バージョン交換 -> KEXINIT でアルゴリズム合意 -> 鍵交換とホスト鍵検証 -> ユーザー認証 -> チャネル」の順に進む
- ホスト鍵の検証は known_hosts による TOFU が基本。RFC 4251 も初回接続を弱点と認めており、初回フィンガープリントは別経路で照合するのが安全
- 公開鍵認証では秘密鍵は流れず、セッション識別子を含むデータへの署名だけを送る。許可鍵は authorized_keys で管理する
- ポートフォワーディングは接続プロトコルのチャネル多重化の応用。
-L(ローカル)、-R(リモート)、-D(SOCKS)を使い分ける - 実務では
~/.ssh/configと ssh-agent で運用を楽にし、ssh -vで失敗箇所を切り分ける
SSH は「鍵さえあればつながる」道具ですが、その裏では鍵交換・ホスト鍵検証・署名という複数の仕組みが噛み合って安全を支えています。各段階が何を保証しているのかが分かると、接続エラーの調査もサーバー側のセキュリティ設定も、迷わず勘所を押さえられるようになります。
参考リンク
- RFC 4251: The Secure Shell (SSH) Protocol Architecture
- RFC 4252: The Secure Shell (SSH) Authentication Protocol
- RFC 4253: The Secure Shell (SSH) Transport Layer Protocol
- RFC 4254: The Secure Shell (SSH) Connection Protocol
- OpenSSH ssh(1) man ページ
- OpenSSH ssh-keygen(1) man ページ
- TCP と UDP の基本 - 3ウェイハンドシェイク(当ブログ)
- 公開鍵暗号と電子署名の仕組み(当ブログ)
- OpenSSH 10.4 の新機能とセキュリティ修正(当ブログ)
- lftp で SFTP サーバーに接続する方法(当ブログ)


