6 ポイント 投稿者 GN⁺ 2024-09-20 | 1件のコメント | WhatsAppで共有
  • SSHトンネリングは安全なSSH接続の上でTCPトラフィックを運び、レガシープロトコルの暗号化・管理パネルへのアクセス・NATの背後にあるサーバーへの接続のように、制限された経路を安全に開くために使われる
  • サーバー側ではAllowTcpForwarding yesが必要で、ループバック以外のインターフェースでポートを開くには**GatewayPorts yes**の設定とSSHサーバーの再起動が必要
  • ssh -J, ssh -L, ssh -R, ssh -Dはそれぞれジャンプホスト、ローカルフォワーディング、リモートフォワーディング、SOCKS5ベースの動的フォワーディングを担当する
  • トンネルをバックグラウンドで維持するにはssh -fNを使い、切断の検知はClientAliveIntervalClientAliveCountMaxのようなハートビート設定で調整する
  • SSHトンネリングはUDPを直接サポートせず、TCP-over-TCPでは遅延やスループットの問題が発生しうるため、VPNの代替というよりは特定のTCP経路を開くためのツールに近い

SSHトンネリングを使う場面

  • SSHトンネリングとポートフォワーディングは、SSH接続を通じてTCPトラフィックをクライアントからサーバーへ、またはサーバーからクライアントへ転送する
  • TCPポートとUNIXソケットを使えるが、例はTCPポートを中心としている
  • 代表的な活用方法はセキュリティ、トラブルシューティング、接続の迂回に分けられる
    • セキュリティ
      • FTPのような安全でない接続やレガシープロトコルを暗号化する
      • 公開鍵認証ベースのSSHトンネルでWeb管理パネルにアクセスする
      • 80/443のような追加ポートを公開せず、22番ポートだけを開けられる
    • トラブルシューティング
      • ファイアウォールやコンテンツフィルタを回避する
      • 別のネットワーク経路を選択する
    • 接続
      • NATの背後にあるサーバーへアクセスする
      • ジャンプホストを通じてインターネットから内部サーバーへ入る
      • ローカルポートをインターネットに公開する

ポートフォワーディング前に確認すべき設定

  • 例のオプションは環境に合わせて組み合わせて設定できる
  • bind_addressを指定しない場合、デフォルト値はlocalhost
  • ローカルとリモートのユーザーは、それぞれのマシンでポートを開く権限が必要
    • 0-1024番ポートは別途設定がなければroot権限が必要
    • それ以外のポートは一般ユーザーでも設定できる
    • クライアントとネットワークのファイアウォールも、フォワーディング経路に合わせて開いている必要がある
  • SSHサーバーではポートフォワーディングが有効になっている必要がある
    • AllowTcpForwarding yes
    • デフォルトで有効なことが多いが、サーバー設定の確認が必要
  • 127.0.0.1以外のインターフェースへポートをフォワーディングするには、SSHサーバーでGatewayPortsを有効にする
    • GatewayPorts yes
    • 設定後はSSHサーバーサービスを再起動する必要がある

SSHジャンプホストと多段トンネル

  • ssh -Jは、1台以上のホストを経由してリモートホストへ透過的に接続するときに使う
ssh -J user@REMOTE-MACHINE:22 -p 22 user@10.99.99.1
  • デフォルトポート22を使う場合はポート表記を省略できる
  • REMOTE-MACHINEをジャンプホストとして使うと、接続は次のように確認できる
[user@REMOTE-MACHINE]$ ss | grep -i ssh
tcp ESTAB 0 0 167.135.173.108:ssh 192.160.140.207:45960
tcp ESTAB 0 0 10.99.99.2:49770 10.99.99.1:ssh
  • 例のアドレスの役割は次のとおり
    • 167.135.173.108: REMOTE-MACHINEの公開IP
    • 192.160.140.207: LOCAL-MACHINEの公開IP
    • 10.99.99.2: REMOTE-MACHINEの内部IP
    • 10.99.99.1: REMOTE-WEBAPPの内部IP
  • 複数のジャンプホストを使う場合はカンマで区切る
ssh -J user@REMOTE-MACHINE:22,user@ANOTHER-REMOTE-MACHINE:22 -p 22 user@10.99.99.1

ローカルポートフォワーディング

  • ローカルポートフォワーディングはssh -Lオプションを使う
  • 1つ目の例では、LOCAL-MACHINEの10.10.10.1:8001をREMOTE-MACHINEのlocalhost:8000へ転送する
ssh -L 10.10.10.1:8001:localhost:8000 user@REMOTE-MACHINE
  • REMOTE-MACHINEで127.0.0.1にのみバインドされたWebサーバーのアクセスログは次のように見える
127.0.0.1 - - [30/Dec/2022 18:05:15] "GET / HTTP/1.1" 200
  • このリクエストはLOCAL-MACHINEから開始される
  • 2つ目の例では、ローカル8001をREMOTE-MACHINE経由で10.99.99.1:8000へ転送する
ssh -L 8001:10.99.99.1:8000 user@REMOTE-MACHINE
  • REMOTE-WEBAPPのWebサーバーアクセスログには、REMOTE-MACHINEの内部IPから来たリクエストとして記録される
10.99.99.2 - - [30/Dec/2022 21:28:42] "GET / HTTP/1.1" 200

リモートポートフォワーディング

  • リモートポートフォワーディングはssh -Rオプションを使う
  • 例ではREMOTE-MACHINEの8000を、ローカル側のlocalhost:8001または10.10.10.2:8001へ転送する
ssh -R 8000:localhost:8001 user@REMOTE-MACHINE
ssh -R 8000:10.10.10.2:8001 user@REMOTE-MACHINE
  • 特定のリモートインターフェースである10.99.99.2:8000で受信するよう設定することもできる
ssh -R 10.99.99.2:8000:10.10.10.2:8001 user@REMOTE-MACHINE
  • ループバックインターフェース以外で受信するには、SSHサーバーで**GatewayPorts yes**が有効になっている必要がある

動的ポートフォワーディングとSOCKS5

  • 複数のポートを転送するとき、SSHはSOCKSプロトコルを使う
  • SOCKSは透過型プロキシプロトコルで、SSHは最新バージョンであるSOCKS5を使用する
  • SOCKS5サーバーのデフォルトポートはRFC 1928で定義された1080
  • クライアントはアプリケーション層またはOS層でSOCKSプロキシを使うよう設定されている必要がある
  • ssh -Dの例では、10.10.10.1:5555にSOCKSプロキシを開く
ssh -D 10.10.10.1:5555 user@REMOTE-MACHINE
  • LOCALクライアントではcurlで接続経路をテストできる
curl -L -x socks5://10.10.10.1:5555 brrl.net/ip
  • 正常に動作すればREMOTE-MACHINEの公開IPが返される

SSH TUN/TAPトンネリング

  • -wフラグで双方向トンネルを作成できる
  • インターフェースは事前に作成されている必要がある
  • この方式には、未テストであるという注意書きがある
-w local_tun[:remote_tun]

バックグラウンド実行と終了

  • トンネルをネイティブな方法でバックグラウンド実行するには-fNを使う
    • -f: バックグラウンド実行
    • -N: シェルを開かない
ssh -fN -L 8001:127.0.0.1:8000 user@REMOTE-MACHINE
  • それ以外にはscreenや別のツールを使える
  • バックグラウンドで実行中のSSHはプロセスを見つけてPIDで終了する
user@pleasejustwork:~$ ps -ef | grep ssh
[...]
user 19255 1 0 11:40 ? 00:00:00 ssh -fN -L 8001:127.0.0.1:8000 user@REMOTE-MACHINE
[...]
kill 19255

SSH接続の維持と再接続

  • SSH接続を維持する方法はいくつかある
  • タイムアウト処理のためのハートビートオプションは、クライアント、サーバー、またはその両方に設定できる
  • ClientAliveIntervalは接続維持のため、n秒ごとにリクエストを送る
ClientAliveInterval 15
  • ClientAliveCountMaxは、相手から応答を受け取れなかったあと、接続を終了するまでに送るハートビート要求数
ClientAliveCountMax 3
  • 3はデフォルト値で、0に設定すると接続終了を無効化する
  • 例の設定では、応答がなければ約45秒後に接続が切れる
  • 接続終了後の再接続にはautossh、スクリプト、cronjobなどを使える

制限とセキュリティ上の注意点

  • UDP

    • SSHは正しい復号のために信頼性のある転送に依存している
    • UDPは信頼性を提供しないため、SSHトンネルではサポートされず推奨もされない
    • UDP over SSH tunnelingを扱った方法もあるが、未テストであるという注意書きがある
  • TCP-over-TCP

    • オーバーヘッドのためスループットが低下し、遅延が増加する
    • パケットロスや高遅延のある接続ではTCP meltdownが発生する可能性がある
    • TCP over TCPに関する記事を参考にできる
    • OpenVPN-over-TCPをしばらく使った際、スループットはUDPより低かったが安定して動作し、結果は構成に大きく依存した
  • VPNの代替として使う際の限界

    • SSHトンネリングをVPNのように使うことはできるが、より良い性能を求めるならVPNのほうが適している
  • セキュリティリスク

    • この機能が不要なら無効化することが推奨される
    • 攻撃者がSSHトンネリングとポートフォワーディングを使ってファイアウォールや他のセキュリティ手段を回避できる
  • 参考文書

1件のコメント

 
GN⁺ 2024-09-20
Hacker News の意見
  • 2024年には、SSH コマンドを長く直接書くよりも、~/.ssh/configLocalForwardRemoteForwardProxyJump を設定するほうがよい
    複数の中継 SSH 接続を経由しなければならないリモートサーバーに sshscprsync でアクセスするとき、時間を大幅に節約できる
    例えば jump-host-1jump-host-2jump-host-3ProxyJump でつないでおき、target-server にエイリアスで接続すればよい
    LocalForward 0.0.0.0:8080 0.0.0.0:80 はリモートの 80 番ポートをローカルの 8080 に転送し、RemoteForward 0.0.0.0:9022 0.0.0.0:22 はリモートの 9022 に入ってきた SSH リクエストをローカルの 22 番へ転送する
    LocalForwardRemoteForward では左側が対象サーバー、右側がローカルで、localhost127.0.0.1 の代わりに 0.0.0.0 を使うという Tips もある

    • ssh_config の Tips を共有すると、自宅内やセルフホストの VPN 内では各機器に直接 SSH し、外からはジャンプホストを経由するよう自動分岐しておく構成が便利
      Match host *.example.org exec "getent ahosts router.example.org | grep -q ^10.0.0.1" のようにルーターのアドレスで自宅/VPN かどうかを先に検出し、arp で MAC アドレスが期待値と違えば ProxyJump jump.example.org を使うようにできる
      順序が重要で、VPN/自宅ネットワークの検出を先に行い、その後に外部の場合のジャンプホストを適用する必要がある
    • ときには一度だけ使って終わる作業なので、設定ファイルまで作りたくない場合もある
      短期間使う VPS を作って SSH を SOCKS プロキシとして使い、地域制限を回避したり、別チームを少し手伝うために普段アクセスしないサーバーへ一度だけ入るような場合は、コマンドラインオプションのほうがよいと思う
    • 0.0.0.0 を使うのは危険な場合がある
      すべてのネットワークインターフェースでポートを開くことができ、特にリモートサーバーにファイアウォールがなければ、何かをインターネット全体に公開してしまう
      よく使うトンネリングやポート転送なら設定ファイルがよいが、一回限りやまれな作業ならコマンドラインオプションのほうがよいと思う
    • こうした SSH 設定を複数のローカルマシンに標準化して配布するには、どう管理しているのか気になる
      単一ユーザー/複数マシンに合い、別サーバーを常時立てておく必要なく GitHub などから設定を取得する、GitOps に似た解決策を探している
    • 常にシェルを開きたいわけではないので、基本的な bash/fish 関数を作って、最小限の引数だけ覚えればよいようにしてある
      忘れたら、文書化しておいた関数定義を見ればよい
  • 私が働いている、とんでもない企業環境では SSH トンネリングが必須
    アクセス権を得たり、ポートを開けたり、ファイアウォール/VPN の例外を得たりするのに、官僚主義のせいで何週間もかかることがある
    この記事は -D に言及しているが、その威力を十分には説明していない
    ssh -D 8888 someserver を実行して、ブラウザの SOCKS プロキシを localhost:8888 に設定すると、ブラウザのトラフィックがすべて someserver 経由でルーティングされる
    Firefox は今でもシステムのデフォルト値を変えずにこの設定ができるので、とても便利

    • 2000年代半ばには、家の外でウェブを見るとき、ほぼ標準的な方法としてそうしていた
      ところが会社に入ってみると、IP 許可リストのせいでインターネット上の任意のサーバーへ SSH 接続することすらできず、あまりにつらくて HTTP トンネルを作り、自分が制御するサーバーを許可リストに入れる方法まで調べたが、結局転職した
    • 企業ネットワークを迂回するのはよい考えではない
      そのような制限には理由があり、迂回は組織の手続きやセキュリティに対する専門性不足や軽視と見なされかねない
      アクセス権を得るのに何週間も何か月もかかるなら、そのように待つべきで、機密情報にバックドアを開けたり、セキュリティを損なったりした責任を負いたくはないはず
  • 深夜3時にやった中で最も汚い SSH トンネリングのハックは、3か所のデータセンターをつなぐ作業だった
    同じ都市圏にある3つの施設はインターネットの上位網には接続されていたが、妙なルーティングポリシーのせいで A は B にしか、B だけが C に接続できた
    結局、A のデータを B 経由で C へ移すために、rsync + SSH トンネル + 鍵 + ルーティングの小細工を混ぜ合わせる必要があり、呪文のようなコマンドを何度か直して合わせたあと、全体が一度に動くのを見たときは魔法のようだった
    今ならどうすればよいかはっきり分かるが、当時初心者だった身としては大きな達成で、同期が完了したのを確認したときの高揚感は今でも覚えている

    • ~/.ssh/configProxyJump を使えば、A、B、C、D、E のように何段階でも実質的にジャンプして移動できる
  • 可視化の細部がよい
    ネットワーキングには、とくにより低レベルの接続でトラフィックを視覚的に表現するツールがもっとたくさんあってほしい

    • ここの図も見る価値がある: https://www.nathanhandy.blog/articles/osi-model-revisited.ht...
      もちろん静的な概念表現にすぎず、ほとんどのネットワークベンダーも何らかの形のトラフィック可視化は提供しているが、たいていは個別の指標やグラフのレベルにとどまる
  • Linux サーバーや IoT 機器がファイアウォールの背後にあり、固定 IP もないが SSH で接続したいなら、https://sshreach.me のようなトンネリングサービスを使える

  • 何年もの間トンネリングをかなり使ってきたが、-J オプションは知らなかった
    数か月に一度トンネルが必要になるたびに30分かけて設定するより、トンネルを構成してくれる視覚的なツールがあればいいのに

  • TCP-over-TCP はオーバーヘッドを増やしてスループットを下げ、レイテンシを高め、パケットロスがある接続や衛星回線のような高遅延の接続では TCP メルトダウンを引き起こし得る、という説明がある
    ただし SSH トンネルでは、TAP/TUN を使わない限り実際には問題にならない
    SSH が TCP ストリームを解いて転送するため
    ただし複数チャネルを使う場合は、ヘッドオブラインブロッキングのため性能が低下することがある

  • 約15年前、大学ネットワークのファイアウォールを回避するために SSH トンネルを覚え、デフォルトポートを 443番 に変える必要があった
    それ以来、ファイアウォール回避よりはるかに多様な用途で使い続けている

    • ファイアウォール回避やローカルサービスをネットワーク越しに公開する以外に、どんな用途で面白い使い方ができたのか気になる
  • sshuttle を使ってみると、トンネリングがずっと楽になる
    sshuttle -r user@host 10.0.0.0/8 のように使うと、10/8 の範囲は自動的にトンネルされ、実質的に SSH 上の VPN のように動作する

  • SSH 自体にリダイレクト機能があるのか気になる
    たとえば A が B に SSH しようとすると、B が C に接続するよう伝え、A が透過的に C へ直接接続し、B はそれ以上コアのデータ経路に残らない、というような機能があるのか知りたい

    • 仮想 IP で似たことはできそう
      自分のファイアウォール/ルーターは DHCP option 67 を正しく転送せず、必ず自分のアドレスを返すため、そのポートの PXE ブートトラフィックがルーター IP に向かったら実際の PXE ブートサーバーへルーティングされるよう、仮想 IP/ルールを設定する必要があった
    • A が実際の宛先が C だと知らないなら、誤解を招く可能性がある
      そうでなければジャンプ機能を使えばよい: ssh -J B C
      B が経路上にいる必要がなく、C に直接接続できるなら単に C へ直接接続すればよく、それができないならいずれにせよ B はホップとして入る必要がある
    • B が C へポート転送、つまりパケットルーティングを行うことはできるだろうが、HTTP の永続リダイレクトのような同等の機能はないように思う
      問題をもう少し説明すれば、より適した解決策があるかもしれない
      ある程度組み込み寄りのホストなら DNS に「ルーティング」を担わせることもできるが、この場合 ホストキー指紋検証 は自分で処理する必要がある