セキュリティ上の理由やネットワーク構成により、目的のサーバーに直接SSH接続できない場合があります。このような場合、一度**踏み台サーバー(Bastion Host / Jump Host)**に接続し、そこから目的のサーバーに接続する「多段SSH」が必要になります。
ここでは、ssh コマンドの ProxyCommand オプションや ProxyJump オプション、またはSSH設定ファイル (~/.ssh/config) を利用して多段SSH接続を行う方法を解説します。
1. ssh コマンドの ProxyCommand オプションを使用する
ProxyCommand オプションは、SSH接続を確立する前に実行されるコマンドを指定します。このコマンドの標準入力/出力が、目的のサーバーへのSSH接続のトンネルとして利用されます。
ssh -o ProxyCommand="ssh -W %h:%p 踏み台サーバーのユーザー名@踏み台サーバーのIPアドレスまたはホスト名" 目的サーバーのユーザー名@目的サーバーのIPアドレスまたはホスト名
踏み台サーバーのユーザー名@踏み台サーバーのIPアドレスまたはホスト名: 踏み台サーバーへの接続情報です。目的サーバーのユーザー名@目的サーバーのIPアドレスまたはホスト名: 最終的に接続したい目的サーバーへの接続情報です。ssh -W %h:%p:sshコマンドの-Wオプションは、標準入力/出力を介してTCPポート転送を行うためのものです。%hは目的サーバーのホスト名、%pは目的サーバーのポート番号に展開されます。
例:
- 踏み台サーバー:
bastion_user@192.168.1.100 - 目的サーバー:
target_user@10.0.0.5
ssh -o ProxyCommand="ssh -W %h:%p bastion_user@192.168.1.100" target_user@10.0.0.5
2. ssh コマンドの ProxyJump オプションを使用する (OpenSSH 7.3以降)
OpenSSH 7.3以降では、ProxyJump オプションが導入され、多段SSH接続がよりシンプルに記述できるようになりました。
ssh -J 踏み台サーバーのユーザー名@踏み台サーバーのIPアドレスまたはホスト名 目的サーバーのユーザー名@目的サーバーのIPアドレスまたはホスト名
例:
ssh -J bastion_user@192.168.1.100 target_user@10.0.0.5
複数の踏み台サーバーを経由する場合も、カンマ区切りで指定できます。
ssh -J user1@host1,user2@host2 target_user@target_host
3. SSH設定ファイル (~/.ssh/config) を利用する (推奨)
~/.ssh/config ファイルに設定を記述することで、複雑なコマンドを毎回入力する手間を省き、エイリアスを使って簡単に接続できるようになります。
~/.ssh/config ファイルを作成または編集します。
nano ~/.ssh/config
以下の内容を記述します。
# 踏み台サーバーの設定
Host bastion
HostName 192.168.1.100
User bastion_user
IdentityFile ~/.ssh/id_rsa_bastion # 踏み台サーバーへの接続に使う秘密鍵 (任意)
# 目的サーバーの設定
Host target
HostName 10.0.0.5
User target_user
ProxyJump bastion # ここで踏み台サーバーのHost名を指定
IdentityFile ~/.ssh/id_rsa_target # 目的サーバーへの接続に使う秘密鍵 (任意)
設定後、以下のコマンドで目的サーバーに接続できます。
ssh target
~/.ssh/config の各オプション
Host: この設定のエイリアス(短縮名)です。sshコマンドでこの名前を使用します。HostName: 実際のホスト名またはIPアドレスです。User: 接続するユーザー名です。IdentityFile: 接続に使用する秘密鍵のパスです。ProxyJump: 踏み台サーバーのHostエイリアスを指定します。OpenSSH 7.3以降で利用可能です。ProxyCommand:ProxyJumpが利用できない古いSSHクライアントの場合や、より複雑なトンネリングが必要な場合に使用します。# ProxyJumpの代わりにProxyCommandを使う場合 Host target_old_ssh HostName 10.0.0.5 User target_user ProxyCommand ssh bastion_user@192.168.1.100 -W %h:%p IdentityFile ~/.ssh/id_rsa_target
~/.ssh/config を利用する方法は、設定を一度行えば再利用性が高く、管理も容易なため、多段SSH接続を行う際には最も推奨される方法です。
4. 検証:ホップ数が増えるとSSH接続はどれだけ遅くなるか
ProxyJump は便利ですが、踏み台を経由するたびに接続確立コストが積み上がります。実際どの程度重くなるのか、本記事の検証環境(本番のリモートサーバーではなくローカルサンドボックス)で実測しました。
検証方法(正直な前提の開示)
このサンドボックスには管理者権限がなく、sshd を起動したりリモートログインを有効化したりできません(ssh localhost は Connection refused になることを確認済みです)。そのため、実際のリモートサーバー2台間で ssh -J を計測する、という理想的な検証はできません。
代わりに、Pythonの SSH 実装
paramiko
を使い、本物の SSH プロトコル(鍵交換 + 公開鍵認証)を実際に実行するベンチマークをローカルループバック上に構築しました。ポイントは次の通りです。
- 踏み台役のサーバーは、
direct-tcpipチャネル要求(=ssh -W host:portやProxyJumpが内部で使う仕組み)を受け取ると、実際に新しいTCPソケットを次のホップへ接続し、中身を見ずにバイト列を右から左へ中継します。これは本物のsshdの-W実装と同じ挙動です。 - クライアントは、その中継されたパイプの上でゼロから独立したSSHハンドシェイク(鍵交換・公開鍵認証)を踏み台ごとに順番に行います。
ssh -J h1,h2,targetが内部で行っているのと構造的に同じです。 - したがって「各ホップが自分自身の接続確立コスト(鍵交換+認証のラウンドトリップ)を追加し、それが直列に積み上がる」という多段SSHの構造は本物です。
一方で、正直に限界も書いておきます。
- 全区間がループバック(127.0.0.1)なので、実際のWAN越しのネットワークRTTは含まれていません。実運用では各ホップにその区間固有のRTTが乗ります。
paramikoは純Python実装であり、システムのOpenSSH(本記事1〜3節のコマンド)とは別の実装です。絶対値はこの実行環境・この実装固有のものであり、本番のssh/sshdの絶対値とは異なります。
再現可能なように、計測コードの中核部分を以下に示します(完全版は踏み台サーバーの起動処理などを含みますが、要点は「チャネルを開いて、その上で新しい Transport を作り直しハンドシェイクする」の繰り返しです)。
import socket
import time
import paramiko
def do_ssh_handshake(sock_or_chan, client_key):
"""1回分の本物のSSHハンドシェイク(鍵交換+公開鍵認証)を実行する"""
t = paramiko.Transport(sock_or_chan)
t.start_client(timeout=10)
t.auth_publickey("bench_user", client_key)
return t
def measure_chain(num_hops, target_port, bastion_ports, client_key):
start = time.perf_counter()
sock = socket.create_connection(("127.0.0.1", bastion_ports[0]), timeout=10)
t = do_ssh_handshake(sock, client_key)
for i in range(1, num_hops):
chan = t.open_channel("direct-tcpip", ("127.0.0.1", bastion_ports[i]), ("127.0.0.1", 0))
t = do_ssh_handshake(chan, client_key) # 踏み台ごとに独立したハンドシェイク
chan = t.open_channel("direct-tcpip", ("127.0.0.1", target_port), ("127.0.0.1", 0))
t = do_ssh_handshake(chan, client_key) # 最後に目的サーバーへハンドシェイク
return time.perf_counter() - start
各ホップ数(直接接続=0ホップ、踏み台1〜3台)につき、ウォームアップ5回を捨てたあと40回ずつ接続確立時間を計測しました。
結果

| 経路 | 中央値 (n=40) | 直接接続比の増分 |
|---|---|---|
| 直接接続(0ホップ) | 6.9 ms | - |
| 踏み台1台(ProxyJump ×1) | 14.7 ms | +7.9 ms |
| 踏み台2台(ProxyJump ×2) | 22.8 ms | +8.1 ms |
| 踏み台3台(ProxyJump ×3) | 33.7 ms | +10.9 ms |
この環境では、踏み台を1台経由するごとにおよそ8〜11msの接続確立コストが追加され、ホップ数にほぼ比例して増加しました。これは「踏み台1台ぶんの鍵交換+公開鍵認証」のコストがそのまま直列に足し込まれることの直接的な証拠です。
実務への示唆
実際のリモート環境では、この増分に各区間のネットワークRTTが上乗せされます。踏み台が地理的に離れているほど、多段化のコストは(このループバック実測値より)はるかに大きくなります。おおまかには次の式で見積もれます。
\[ T_{total} \approx \sum_{i=1}^{N+1} \left( \text{RTT}_i + T_{kex,i} + T_{auth,i} \right) \](\(N\) = 踏み台の台数、最後の項は目的サーバーへの接続分)
1回きりの接続であればこのコストは無視できますが、スクリプトが同じ多段経路に対して接続を何度も張り直す場合(例: Ansibleのタスクごと、CIのステップごとなど)、この増分が回数分積み重なります。
対策として、
~/.ssh/configにControlMaster auto/ControlPersist 10mを設定し、コネクションを多重化・再利用するのが有効です。最初の1回だけハンドシェイクのコストを払い、以降の接続は既存のマスター接続に相乗りするため、上記の増分をほぼゼロにできます。Host bastion target ControlMaster auto ControlPath ~/.ssh/sockets/%r@%h-%p ControlPersist 10m
よくある質問(FAQ)
ProxyJumpとProxyCommandはどちらを使うべき?
OpenSSH 7.3以降であれば、コマンドがシンプルな ProxyJump(-J オプション)を使うのが基本です。ProxyCommand は、ProxyJump が利用できない古いSSHクライアントの場合や、より複雑なトンネリングが必要な場合に使用します。さらに日常的な運用では、どちらを直接コマンドラインで打つのではなく、~/.ssh/config にHostエイリアスとして設定しておく方法が、再利用性・管理のしやすさの点で最も推奨されます。
多段SSHでscp/rsyncは使える?
使えます。本記事で解説した ~/.ssh/config のHostエイリアス設定(ProxyJump を含む)はOpenSSHの接続確立ロジックそのものに適用されるため、内部で ssh コマンドを利用する scp や rsync からも同じ設定を再利用できます。scp は ssh と同じく ~/.ssh/config を直接参照するため、Hostエイリアスを指定するだけで踏み台経由の転送が行えます。rsync も既定でリモートシェルに ssh を使う(-e ssh)ため、同様にHostエイリアスを指定すれば ProxyJump の設定に従って多段接続されます。
踏み台を2段以上経由するには?
ssh -J オプションはカンマ区切りで複数の踏み台サーバーを指定できます(例: ssh -J user1@host1,user2@host2 target_user@target_host)。~/.ssh/config では、目的サーバーの ProxyJump に踏み台のHostエイリアスを指定する構成を踏み台の数だけ連鎖させることで、同様に多段構成を組めます。なお本記事の検証では、踏み台1台ごとにおよそ8〜11msの接続確立コストが直列に積み上がることを実測しており、ホップ数が増えるほど ControlMaster によるコネクション再利用の効果が大きくなります。