TT Lab
はじめる
学ぶ 学習パス コース

SSHとファイル転送

-L、-R、-Dを混同しない方法

TT Labで続きを見る

一言でいうと

-Lは自分側のポートを開いて向こうへ送り、-Rは向こう側のポートを開いて自分側へ引き込み、-Dは自分側にSOCKSプロキシを立てます。方向さえ正確に押さえれば、残りは文法です。

なぜ必要なのか

社内ネットワークのDBに接続したいのに、そのDBは外からアクセスできません。踏み台サーバー(bastion)にはSSHで接続できます。このとき必要なのが、ローカルフォワーディングです。自分のノートPCの15432に入ってきた接続を、踏み台サーバーを経由してdb.internal:5432へ送ります。

ssh -L 15432:db.internal:5432 bastion

読み方: -L <내가 열 포트>:<배스천이 볼 때의 목적지>:<그 포트>(プレースホルダーは順に、自分が開くポート、踏み台サーバーから見た宛先、そのポートです)。途中のホスト名はSSHサーバーを基準に解釈されるという点が核心です。自分のノートPCでdb.internalが解決できなくても、問題ありません。

どう動くのか

オプション ポートが開く場所 トラフィックの方向 代表的な用途
-L ローカル ローカル → SSHサーバー → 宛先 社内ネットワークのDB、管理コンソールへのアクセス
-R リモート(SSHサーバー) リモート → SSHサーバー → ローカル → 宛先 ファイアウォールの裏のサービスを外に公開、Webhookの受信
-D ローカル(SOCKS) アプリケーションが選んだ任意の宛先 ブラウザー全体を社内ネットワーク経由にする

SSHフォワーディングの3つの方向。-Lは自分のコンピューターにポートを開き、SSHサーバーを経由して向こうのネットワークの宛先へ送り出し、-Rは逆にSSHサーバーにポートを開いて、入ってきたものを自分のコンピューター側の宛先へ引き込み、-Dは自分のコンピューターにSOCKSプロキシを開いて、アプリケーションが選んだ複数の宛先へ出ていく

トンネル専用の接続には、-N -fを一緒に使います。-Nはリモートコマンドを実行せず、-fはバックグラウンドに送ります。

ssh -N -f -L 15432:db.internal:5432 bastion

-Rには落とし穴が1つあります。デフォルトでは、SSHサーバーのループバックにだけバインドされます。つまり、サーバー上の他の人は、そのポートを使えません。外に開くには、サーバーでGatewayPorts yesを有効にする必要があり、これはセキュリティ上、慎重にする必要があります。

サーバー側でフォワーディングを統制するディレクティブです。

AllowTcpForwarding no
AllowAgentForwarding no
GatewayPorts no
PermitOpen 10.0.5.20:5432

PermitOpenが特に実用的です。フォワーディングを完全には塞がずに、宛先を許可リストで制限します。「開発者は本番DBには接続する必要があるが、ほかの社内サービスにはアクセスしてはいけない」という状況に、ぴったり合います。

エージェントフォワーディングは危険

-A(エージェントフォワーディング)は便利ですが、経由サーバーのroot権限を持つ人が、自分のエージェントソケットを通して、自分の鍵で別のサーバーに認証できてしまいます。踏み台サーバー経由が目的なら、ProxyJump(-J)を使ってください。エージェントを公開せずに、同じ結果が得られます。

ssh -J bastion deploy@10.0.3.14

エスケープシーケンス

トンネルを使っていると、接続が止まったように見えることがあります。このときに使うのが、エスケープシーケンスです。行の一番最初で~を押してから、次の文字を入力します。

シーケンス 動作
~. 接続を強制終了(止まったセッションから脱出)
~^Z SSHをバックグラウンドへ
~# フォワーディング中の接続の一覧
~C コマンドラインを開く(実行中に-Lを追加/削除)
~? ヘルプ

~Cで接続中にフォワーディングを追加できることを知っている人は、意外と少ないです。セッションを切らずに、トンネルをもう1つ掘る必要があるときに便利です。

3つの方向を混同しない方法

-L、-R、-Dは、文字が違うだけでなく、やることがまったく違います。どちら側で待ち受けて、どちら側から出ていくかだけを押さえれば、混同しません。

オプション 待ち受ける場所 出ていく場所 使う状況
-L 5432:db:5432 自分のコンピューター リモートサーバー 自分が届かないDBに接続する
-R 8080:localhost:3000 リモートサーバー 自分のコンピューター 自分の開発サーバーを外から見えるようにする
-D 1080 自分のコンピューター(SOCKS) リモートサーバー 複数の宛先を一度に扱う

-Lのdb:5432はリモートサーバーから見える名前です。自分のhostsファイルではなく、サーバーのDNSで解決されます。ここでずれる場合が最も多いです。

なぜリモートポートが外から開かないのか

-R 8080:localhost:3000を設定して、別の機器から接続したのに拒否されるのは、設定の問題ではなくデフォルトです。sshdは、GatewayPorts noのもとでは、転送ポートをリモートサーバーのループバックにだけ開きます。サーバーの/etc/ssh/sshd_configでGatewayPorts clientspecifiedに変更し、-R 0.0.0.0:8080:localhost:3000のようにアドレスを明示して初めて、外へ開きます。この値を有効にするのは、そのサーバーに接続できる人なら誰でも、自分のノートPCのポートをインターネットに公開できるようになるという意味なので、共用の踏み台サーバーでは有効にしません。

切れたトンネルを復活させるのはsshではありません

トンネルは必ず切れます。NATのテーブルが期限切れになり、無線が切れ、サーバーが再起動します。ssh自体は再接続しないので、開き直す仕事は外側が担います。

autossh -M 0 -N \
  -o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
  -o ExitOnForwardFailure=yes \
  -L 5432:db.internal:5432 jump.example.com

ServerAliveIntervalは、静かな接続が死んでいないかを確認する仕組みです。15秒ごとに問い合わせて、3回応答がなければ切断します。ExitOnForwardFailure=yesは、ポートを開けなかったときに、接続を成功とみなさないようにします。これを抜くと、トンネルのないまま生きているsshが残って、接続はできているのに何も通らない状態になります。

多重化で接続を節約する

同じサーバーに繰り返し接続するなら、接続を1つ共有して使います。

Host jump
  ControlMaster auto
  ControlPath ~/.ssh/cm-%r@%h:%p
  ControlPersist 10m

2回目の接続からは、ハンドシェイクと認証を飛ばすので、体感が大きく変わります。ただし、ControlPathはソケットファイルなので、ホームディレクトリがNFS上にあると動作しません。その場合は、/run/user/$UIDの下に移します。

現場での姿

トンネルを開けたまま忘れる。-N -fでバックグラウンドに送ったsshプロセスは、静かに生き続けます。数週間後に、「誰がこのポートを開けたのか」になります。ss -ltnpでプロセスがsshと表示されていれば、トンネルです。チームのルールとして、ControlPathやプロセス名に用途を残しているところもあります。

次のラボですること

ローカル・リモート・ダイナミックの3つの方向のフォワーディングをすべて作ってみて、ssで各トンネルがどこにポートを開いたかを確認します。最後に、3つの方式の比較表を、実際の証拠とともに提出します。