SSH ポートフォワーディング入門 - ローカル・リモート・動的転送
この記事で解決できること
-L(ローカル)・-R(リモート)・-D(動的)の 違いと使い分け が分かる- 踏み台越しの DB 接続・社内サービスの一時公開・SOCKS プロキシを 実例で再現 できる
- 「つながらない」ときの 切り分け手順 が身につく
結論(3 つのトンネルの型)
- 手元 → 遠くのサービスに繋ぎたい →
-L(ローカル転送) - 遠くから手元のサービスを見せたい →
-R(リモート転送) - ブラウザ全体をトンネル経由にしたい →
-D(動的・SOCKS)
前提(対象環境)
- OS:Ubuntu(OpenSSH クライアント / サーバ)
- SSH でログインできる状態
- ポート番号は例。実環境に合わせて読み替えること
SSH ポートフォワーディングとは?
結論: SSH の暗号化トンネルに任意の TCP 接続を相乗りさせる仕組み。直接届かないポートへ、SSH 接続だけを頼りに安全に到達できる。
ファイアウォールやプライベートネットワークの内側にあるサービスは、外から直接は叩けない。だが SSH で入れるなら、その SSH 接続の中に別の TCP 通信を通せる。これがポートフォワーディング(トンネリング)。
転送には 3 種類ある。方向と「待ち受ける側」が違うだけで、考え方は共通。
| 種類 | オプション | 待ち受け | 用途 |
|---|---|---|---|
| ローカル | -L |
手元 | 遠くのサービスを手元から使う |
| リモート | -R |
リモート | 手元のサービスを遠くに見せる |
| 動的(SOCKS) | -D |
手元 | ブラウザ等を丸ごとトンネルに通す |
ローカル転送(-L)はどう使うのか?
結論:
-L 手元ポート:宛先ホスト:宛先ポートの形。手元のポートへの接続が、SSH 先から見た宛先へ転送される。踏み台越しの DB 接続が代表例。
基本形
$ ssh -L 8080:localhost:80 user@server
これで 手元の localhost:8080 への接続が、server 上から見た localhost:80 に届く。ブラウザで http://localhost:8080 を開けば、server の 80 番にアクセスしたのと同じになる。
宛先ホスト は SSH 接続先(server)から見た 名前で解決される。localhost なら server 自身、別名なら server から到達できる別ホストを指す。
実例:踏み台越しに DB へ繋ぐ
DB(db.internal:5432)が踏み台 bastion の内側にあり、手元からは直接届かないケース。
$ ssh -L 15432:db.internal:5432 user@bastion
別ターミナルで、手元の 15432 を本物の DB のように扱える。
$ psql -h localhost -p 15432 -U dbuser appdb
トンネルだけ張りたいとき(-N / -f)
ログインシェルは不要でトンネルだけ欲しい場合は -N、バックグラウンドに回すなら -f を足す。
$ ssh -fN -L 15432:db.internal:5432 user@bastion
-N:リモートコマンドを実行しない(転送専用)-f:認証後にバックグラウンドへ回す
-f で背後に回したトンネルは、用が済んだら必ず止める。プロセスを探して終了する。
$ ps aux | grep "ssh -fN" $ kill <PID>
リモート転送(-R)はどう使うのか?
結論:
-R リモートポート:宛先ホスト:宛先ポートの形。リモート側で待ち受け、手元から見た宛先へ転送する。ローカル開発中の Web を一時的に外へ見せる用途が定番。
方向がローカル転送の逆。リモート側のポートへの接続が、手元(SSH を実行したマシン)から見た宛先に届く。
基本形
$ ssh -R 8080:localhost:3000 user@server
server 上の localhost:8080 への接続が、手元の localhost:3000 に転送される。ローカルで動かしている開発サーバ(3000 番)を、server からアクセスできるようになる。
外部公開したいときの落とし穴:GatewayPorts
デフォルトでは -R の待ち受けは リモートのループバック(127.0.0.1)に限定 される。server 自身からは見えるが、server の外からは届かない。server の全インターフェースで受けたい場合は、サーバ側 /etc/ssh/sshd_config の設定が要る。
GatewayPorts yes
設定変更後は sudo systemctl reload ssh で反映する。
GatewayPorts yes は手元のサービスを外部ネットワークへ晒す。公開範囲・認証・期間を限定し、不要になったら即座にトンネルと設定を戻すこと。
動的転送(-D)とは?
結論:
-D ポートで手元に SOCKS プロキシを立てる。宛先ごとにトンネルを張らず、アプリの通信をまとめて SSH 先から出させる。
-L は「1 ポート 1 宛先」だが、-D は宛先を固定しない。手元に SOCKS プロキシができ、それを向いたアプリの通信はすべて SSH 先を出口にして流れる。
$ ssh -D 1080 user@server
これで localhost:1080 が SOCKS5 プロキシになる。ブラウザや curl のプロキシ設定をここに向ける。
$ curl --socks5-hostname localhost:1080 https://example.com
--socks5-hostname は 名前解決もプロキシ側で 行う。社内 DNS でしか引けないホストへ届かせたいときはこちら。単なる --socks5 だと手元で名前解決してしまう。
つながらないときの切り分けは?
結論: 「ポートが衝突」「宛先名の解決位置を誤解」「リモート公開設定の不足」の 3 つが大半。エラー文言から原因を一発で絞り込む。
bind: Address already in use
手元(または -R ならリモート)の待ち受けポートが既に使われている。別ポートに変えるか、専有プロセスを特定する。
$ ss -tlnp | grep :8080
channel ... open failed: connect failed
トンネルは張れたが、SSH 先から宛先へ届いていない。宛先ホスト名・ポート・到達性を SSH 先で確認する。
$ ssh user@server $ nc -vz db.internal 5432
-R なのに外部から繋がらない
前述の GatewayPorts 不足が大半。-R 0.0.0.0:8080:... のように bind アドレスを明示しても、サーバ側設定が no のままなら効かない。
よくある誤解:宛先名はどちらから解決される?
-L 15432:db.internal:5432のdb.internalは SSH 先(server) から解決-R 8080:localhost:3000のlocalhost:3000は 手元 から解決
「手元では引けるのに繋がらない」ときは、解決する側を取り違えていることが多い。
設定を ~/.ssh/config に固定するには?
結論: 毎回長いオプションを打つ代わりに、
LocalForward/RemoteForward/DynamicForwardを config に書けばssh hostだけで済む。
Host db-tunnel
HostName bastion.example.com
User user
LocalForward 15432 db.internal:5432
Host socks
HostName server.example.com
User user
DynamicForward 1080
以後はホスト名を指定するだけ。
$ ssh -fN db-tunnel
詳しい config の書き方は ~/.ssh/config 活用術 を参照。
まとめと安全テンプレ
結論: 方向で
-L/-Rを選び、宛先を固定しないなら-D。トンネルは張りっぱなしにせず、用が済んだら止める。
コピペ用テンプレ
# ローカル転送(遠くのDBを手元へ) ssh -fN -L 15432:db.internal:5432 user@bastion # リモート転送(手元の開発サーバを遠くへ) ssh -R 8080:localhost:3000 user@server # 動的転送(SOCKSプロキシ) ssh -D 1080 user@server