-L、-R、-Dを混同しない方法
一言でいうと
-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) | アプリケーションが選んだ任意の宛先 | ブラウザー全体を社内ネットワーク経由にする |
トンネル専用の接続には、-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つの方式の比較表を、実際の証拠とともに提出します。