多段 ssh / rsync するために ProxyCommand を使ってみる (2)

さーて,どんどんこんらんさせていきますよ。

高度な例 (3) - 多段 ssh

要件は*1

  • host:gw1 に user:foo というアカウントがある
  • host:gw2 に user:bar というアカウントがある
  • host:target に user:baz というアカウントがある
  • user:dayflower が host:local から host:gw1, host:gw2 を経由して host:target に user:baz で アクセスしたい

ProxyCommand をカスケードして指定していけばなんとでもなります。実行例は下記のとおり((コマンドラインから実行する場合,実用上の理由から %h%p などの展開は利用していません))。

local% ssh -o "ProxyCommand ssh -o 'ProxyCommand ssh -l foo -i foo.id_dsa gw1 nc gw2 22'
                                -l bar -i bar.id_dsa gw2 nc target 22"
           -l baz -i baz.id_dsa target

Enter passphrase for key 'foo.id_dsa': ********

Enter passphrase for key 'bar.id_dsa': ********

Enter passphrase for key 'baz.id_dsa': ********

Last login: Fri Feb  8 12:01:55 2008 from gw2

baz@target% ■

これはひどいコマンドラインですね(適宜改行しています)。


以下のようなフローになっています。

  1. (最終的に)target につなぎたい ssh クライアントが立ち上がる
    • [1] ssh [to target]
  2. target に ssh でつなぐためには以下のサブプロセスが必要となる
    • [2] ssh -o 'ProxyCommand ssh gw1 nc gw2 22' gw2 nc target 22
  3. [2]ssh プロセスは gw2 につなぐ必要がある
    • [2] ssh [to gw2]
  4. gw2 に ssh でつなぐために以下のサブプロセスが必要になる
    • [3] ssh gw1 nc gw2 22

実際にプロセスがどのようになっているか確認してみます。

local% ps -efww

  PID  PPID CMD
31448   600 [1] ssh -o ProxyCommand ssh -o 'ProxyCommand ssh -l foo -i foo.id_dsa
                                                             gw1 nc gw2 22'
                                        -l bar -i bar.id_dsa gw2 nc target 22
                    -l baz -i baz.id_dsa target
31449 31448 [2] ssh -o ProxyCommand ssh -l foo -i foo.id_dsa gw1 nc gw2 22
                    -l bar -i bar.id_dsa gw2 nc target 22
31450 31449 [3] ssh -l foo -i foo.id_dsa gw1 nc gw2 22

ProxyCommand で指定されたプロセスが全て host:local で立ち上がっているのが直感的に間違っている気もしますが,問題ありません。なぜなら

  • [3][2] の(local が gw2 に繋ぐ)ためのトランスポートレイヤとして働く
  • したがって [2] で local ⇒ gw2 にダイレクトに繋がる
  • [2][1] の(local が target に繋ぐ)ためのトランスポートレイヤとして働く
  • したがって [1] で local ⇒ target にダイレクトに繋がる

だからです。(下位トランスポートがどのようになっているかに関わらず)[1] ssh は host:target にダイレクトに接続しているつもりなので,よいのです。

コネクションを見てみる

まずは host:local におけるコネクションの様子から。

local% netstat -atn | grep ':22'
Active Internet connections (servers and established)
Proto Local Address  Foreign Address  State      
tcp   local:57530    gw1:22           ESTABLISHED 

ネットワークコネクションとしては,あくまで,host:local から host:gw1 にのみ張られています。


次に,host:gw1 におけるプロセスとネットワークコネクションの様子をみてみます。

gw1% ps -efw | grep foo
UID        PID  PPID  C STIME TTY          TIME CMD
root      8061  1909  0 12:04 ?        00:00:00 sshd: foo [priv]
foo       8063  8061  0 12:04 ?        00:00:00 sshd: foo@notty
foo       8064  8063  0 12:04 ?        00:00:00 nc gw2 22

gw1% netstat -atn | grep ':22'
Active Internet connections (servers and established)
Proto Local Address  Foreign Address  State      
tcp   gw1:43157      gw2:22           ESTABLISHED 
tcp   gw1:22         local:57530      ESTABLISHED 

host:gw2 の 22 ポートに対するコネクションが nc コマンドによって(user:foo の権限で)張られています。また,先ほどの host:local ⇒ host:gw1 のコネクションの受け口も表示されています(ポート番号に着目)。


次に,host:gw2 におけるプロセスとネットワークコネクションの様子です。

gw2% ps -efw | grep bar
UID        PID  PPID  C STIME TTY          TIME CMD
root      2247  1921  0 12:04 ?        00:00:00 sshd: bar [priv] 
bar       2264  2247  0 12:05 ?        00:00:00 sshd: bar@notty  
bar       2265  2264  0 12:05 ?        00:00:00 nc target 22

gw2% netstat -atn | grep ':22'
Active Internet connections (servers and established)
Proto Local Address  Foreign Address  State      
tcp   gw2:38172      target:22        ESTABLISHED 
tcp   gw2:22         gw1:43157        ESTABLISHED 

host:target の 22 ポートに対するコネクションが nc コマンドによって張られています。host:gw1 ⇒ host:gw2 のコネクションの受け口も表示されています。


最後に,host:target でのネットワークコネクションの様子です。

target% netstat -atn | grep ':22'
Active Internet connections (servers and established)
Proto Local Address  Foreign Address  State      
tcp   target:22      gw2:38172        ESTABLISHED 

あくまで explicit なネットワークコネクションとしては host:gw2 から張られた ssh コネクションのみです。


直感とは裏腹に,うまくいってますね。

ようやく ssh_config を書いてみる

$HOME/.ssh/config ではなく,カレントディレクトリに ssh_config という別設定ファイルを置いた場合((このため設定ファイル中で -F ssh_config というオプションが指定されています。通常の設定ではこれらは必要ありません。))の,設定例です。

Host gw1
    User foo
    IdentityFile foo.id_dsa

Host gw2
    ProxyCommand ssh -F ssh_config gw1 nc %h %p
    User bar
    IdentityFile bar.id_dsa

Host target
    ProxyCommand ssh -F ssh_config gw2 nc %h %p
    User baz
    IdentityFile baz.id_dsa

あーーーーー,最初っから ssh_config を書けば一目瞭然でした。

  • host:gw1 に繋ぐためには user:foo で繋ぐ必要があることを示している(通常必要ない設定)
  • host:gw2 に繋ぐためには host:gw1 で nc (on ssh) する必要があることを示している
  • host:target に繋ぐためには host:gw2 で nc (on ssh) する必要があることを示している

これらがカスケード実行されているだけです。


上記の例ではわざわざ gateway で異なるユーザを指定しました。が,同じユーザを使うことが多いでしょうし,実際の環境では %h の展開が理想どおりの値となることは少ないと思います。なので,より現実的な例を。

Host gw2
    ProxyCommand ssh gw1 nc gw2.dmz.network %p

Host target
    ProxyCommand ssh gw2 nc target.inside.network %p
  • host:gw2 は host:gw1 にとって gw2.dmz.network という名前で見えている
  • host:target は host:gw2 にとって target.inside.network という名前で見えている

という例ですが,このように %h 等の展開マクロは使わずに実名で書いた方が混乱は少ないと思います。

*1:ほんとにこんな凝ったシチュエーションがあるかどうかはかなり疑問ですが