SSH ポートフォワーディング入門 - ローカル・リモート・動的転送

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 で反映する。

動的転送(-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:5432db.internalSSH 先(server) から解決
  • -R 8080:localhost:3000localhost: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

次に読む