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

以前

2.(gateway で netcat を ssh 経由で実行することによる転送)はよくわからないけど失敗

多段 rsync がめんどくさい - daily dayflower

と書きましたが,なんとなく仕組みがわかってきたので書きます。

2年前くらいに流行ってたネタなので今更感満点。

まとめ

  • 単純に到達できない場所に ssh でつなぐために ProxyCommand という [http://www.openbsd.org/cgi-bin/man.cgi?query=ssh_config:title=ssh_config] の設定子が使える
  • ProxyCommand とは ssh クライアントと標準入出力でやりとりする
  • 多段 ssh をする際に ProxyCommand で指定すると有用なものとして下記のものがある

2012-04-06 追記: OpenSSH 5.4 以降がクライアントの場合,ssh -W <target_host>:<port> <gateway_host> で netcat 系のコマンドを使わずに同様のことができます。ゲートウェイホスト上の ssh のバージョンが -W オプションをサポートしていなくても大丈夫。

ssh の動作を図で示してみる

単純に remote という host に対して local から ssh で接続し,hostname コマンドを実行してみます。

% hostname

local

% ssh remote hostname
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********

remote

このとき,どのようになっているかということを簡略化して図で示してみます。

ー→[ssh] Local   12345 =→ Remote  [sshd]ー→[hostname]
←ー[   ] Machine ←===22 Machine [    ]←ー[        ]

だいたいいわんとすることはわかりますよね。

ProxyCommand とは

ssh_config の説明から抜粋します。

ProxyCommand

サーバへの接続に利用するコマンドを指定します。コマンド文字列は ユーザのシェル環境で実行されます。コマンド文字列の '%h' は接続先のホスト名で置換されます。'%p' はポート番号で置換されます。コマンドは基本的にどのようなものでもかまいませんが,標準入力から入力をうけつけ標準出力に出力するものである必要があります。そして最終的にはどこかのマシンで動いている sshd サーバや inetd 経由の sshd -i に接続する必要があります(※)。通常,接続先のホストの HostName(たいがいユーザがコマンドラインで指定した名前そのものになりますが)を利用して host key 管理を行います。コマンドに「none」と指定すると,この機能は完全に無効になります。ProxyCommand を利用した場合には,CheckHostIP オプションが利用できないことに注意してください。

この設定子は nc(1) とそのプロキシー機能を絡めて利用すると便利です。たとえば下記のような設定をすると 192.0.2.0 の HTTP(S) プロキシを通じて接続することになります:

ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p
ssh_config(5) - OpenBSD manual pages

重要なのは,『標準入力から入力をうけつけ標準出力に出力するものである必要があります』というところでしょうか。

また,※部分ですが,実際に invoke されたコマンドが sshd に接続しているのを確かめているわけではなく*1,最終的に SSH プロトコルでやりとりする必要がある,ということを意味しているのだと思います。

nc (netcat) とは?

今,例にでてきた nc というのはどのようなコマンドでしょうか。

私の環境にはもともとインストールされていたので,実際に nc を使ってみましょう。POP3 にアクセスしてみます。

% nc mail-server 110

+OK <...>
QUIT                  ← ユーザの入力
+OK

いやぁ,この例だと telnet と変わらないわけですね。

重要なのは,nc (netcat) コマンドは,ネットワークのやりとりを標準入出力にリダイレクトしてくれる,ということです。つまり,

 <stdin>ー→[netcat] Local   <socket> 12345 =→ <socket> Remote
<stdout>←ー[      ] Machine <socket> ←= ????? <socket> Machine

こんなイメージです。


私の使用している RedHatFedora / CentOS では nc がインストールされていました。netcat には様々な実装が存在するのですが,

という経緯で添付されているようです。

OpenBSD nc には今述べたように SOCKS proxy client や HTTP(S) proxy client として振る舞う機能,ポートスキャンする機能もあります。が,ProxyCommand と絡めて多段 ssh をするためだけであれば,『ネットワークのやりとりを標準入出力にリダイレクト』する機能さえあれば充分です。なので,SOCKS プロキシ機能のない古典的な *Hobbit* netcat や GNU netcat でも OK です。


OpenBSD nc や netcat がないよ,という人は gotoh さん作の connect コマンドを利用するのも手でしょう(こちらにも SOCKS proxy や HTTPS proxy 機能があります)。

などに connect コマンドを使用した説明があります。私の環境では nc がいたので,以後 nc を使って説明します。

また,設定を ssh_config に記述せず,-o オプションでコマンドラインにガシガシ書いていきます。かえって見づらくなってしまった感がありますが。

netcat を使ったあまり意味がないが単純なサンプル

ここで要求される条件は,

  • 接続元ホスト local に nc がインストールされている
  • (接続先ホスト remote に nc がインストールされている必要はない)

です。

ProxyCommand に nc を指定してみます。

% ssh -o 'ProxyCommand nc %h %p' remote hostname
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********

remote

無事 host:remote にアクセスできました。

このとき,接続図はこのようになっています。

ー→[ssh]ー→[netcat] Local   12345 =→ Remote  [sshd]ー→[hostname]
←ー[   ]←ー[      ] Machine ←===22 Machine [    ]←ー[        ]

ssh クライアントが ProxyCommand で指定された nc をサブプロセスで立ち上げ,標準入出力を通じてやりとりします。nc はターゲットホストである host:remote の 22(ssh) ポートに接続します。

ssh クライアントは(バックエンドの通信がどのような経路をたどっているかに関わらず)host:remote の sshd プロセスと直接通信を行っている気分になっています。SSH プロトコルによりハンドシェイク・ネゴシエーション・通信を行い,hostname コマンドを host:remote の sshd に実行してもらいます。

netcat を使った2段 ssh

目的は以下のとおりです。

  • user:dayflower が host:local から host:gateway を経由して host:target に ssh でアクセスしたい

ここで要求される条件は,

  • host:gateway に nc がインストールされている
  • (host:local に nc がインストールされている必要はない)
  • host:gateway から host:target へ reachable である
  • (host:gateway からみて target という名前でアクセスできる)
  • (host:local から host:target へ reachable である必要なはい)
  • (host:local が host:target という名前を解決できる必要はない)
  • user:dayflower が host:gateway に存在し id_dsa.pub が authorized_keys として登録されている
  • user:dayflower が host:target に存在し id_dsa.pub が authorized_keys として登録されている

となります。

名前うんぬんは,まぁ IP アドレスとかでいいんですけど。


このとき ProxyCommand として,「ssh 経由ホスト nc %h %p」を指定します((実際には gssapi で最初に認証しようとして時間がかかってしまうので -o 'PreferredAuthentications publickey' というオプションも指定しています。))。

% ssh -o 'ProxyCommand ssh gateway nc %h %p' target hostname
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********

target

この例では ssh-agent を使用していないので,host:gateway と host:target との認証のために,秘密鍵(id_dsa)のパスフレーズを2度聞かれています。ともかく,うまくいきました。

今回の接続を図解してみます。

→[ssh]→[ssh] Local   12345 =→ Gateway [sshd]→[nc] Gateway 23456 =→ Target  [sshd]ー→[hostname]
←[ A ]←[ B ] Machine ←===22 Machine [ C  ]←[  ] Machine ←===22 Machine [ D  ]←ー[        ]

ポイントは,

  1. [ssh B] ←→ [sshd C] 間と,[ssh A] ←→ [sshd D] 間の接続は,それぞれ暗号化されている
  2. [ssh A][sshd D] は直接接続していると思い込んでいる(あくまで比喩として)
    • 実際にはその通信のトランスポートレイヤとして [ssh B]←→[sshd C]←→[nc] が存在する

とくに 2. が重要なのですが,ssh A と sshd D は(仮想的に)直接 SSH プロトコルでやりとりしているわけです。2段目の認証の際も,host:local で秘密鍵パスフレーズにより解読し,host:target の sshd D に送っています。つまり,host:gateway秘密鍵を置いておく必要はありません*2


ここで host:gateway の様子を見てみましょう。

hostname コマンドだと一瞬で終了してしまうので,リモート実行コマンドを指定せずシェルとしてつないでみます。

% ssh -o 'ProxyCommand ssh gateway nc %h %p' target
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********

Last login: Thu Feb  7 14:12:10 2008 from gateway
target% ■

次に,host:gateway にログインして,プロセスの様子をみてみます。

% ssh gateway
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********

Last login: Thu Feb  7 14:12:30 2008 from local
gateway% ps -efw

UID         PID  PPID  C STIME TTY          TIME CMD
root      16105  1921  0 14:18 ?        00:00:00 sshd: dayflower [priv] 
dayflower 16107 16105  0 14:18 ?        00:00:00 sshd: dayflower@notty  
dayflower 16108 16107  0 14:18 ?        00:00:00 nc target 22

たしかに host:gateway で nc コマンドが実行されていること,nc コマンドが host:target に接続していることがわかります。

高度な例 (1) - 別アカウントでログインする

要件は

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

条件は,

  • host:gateway に nc がインストールされていて,host:target に reachable
  • user:foo の公開鍵 foo.id_dsa.pub が host:gateway の authorized_keys に登録されている
  • user:baz の公開鍵 baz.id_dsa.pub が host:target の authorized_keys に登録されている
  • host:local に user:foo の秘密鍵 foo.id_dsa が存在する
  • host:local に user:baz の秘密鍵 bz.id_dsa が存在する

となります。

ProxyCommand には,いままでの応用として -l user オプションだの -i identity_file などを加えたものを指定すればよいです。

% ls

baz.id_dsa  foo.id_dsa

% ssh -o 'ProxyCommand ssh -l foo -i foo.id_dsa gateway nc %h %p' \
      -l baz -i baz.id_dsa target
Enter passphrase for key 'foo.id_dsa': ********
Enter passphrase for key 'baz.id_dsa': ********

Last login: Thu Feb  7 14:57:47 2008 from gateway
baz@target% ■

秘密鍵は host:local においとけば済むっていうのがこんなに幸せとは。

高度な例 (2) - 2段 rsync

rsync-e オプションにいままででっちあげてきた sshコマンドラインをつっこめば OK です。ただし,接続先ホスト(host:target)は -e オプションのほうには含めないでおきます。

% rsync -n -v -a
        -e "ssh -o 'ProxyCommand nohup ssh -l foo -i foo.id_dsa gateway nc -w 5 %h %p'
                -l baz -i baz.id_dsa"
        target:tmp .

nohup: redirecting stderr to stdout
Enter passphrase for key 'foo.id_dsa': ********
Enter passphrase for key 'baz.id_dsa': ********

receiving file list ... done
tmp/

... snip ...

tmp/dummy/

sent 176 bytes  received 798 bytes  278.29 bytes/sec
total size is 1095604  speedup is 1124.85

nohup については後述します。また,念のために nc コマンドに -w 5 として,5秒以上通信がなかった場合に接続が切れるようにしてあります。

高度な例 (3) - 多段 ssh

長くなるので後日書きます。

書きました⇒多段 ssh / rsync するために ProxyCommand を使ってみる (2) - daily dayflower

高度な例 (4) - ProxyCommand が SOCKS proxy を使う

長くなるし面白いので後日書きます。

高度な例 (5) - ProxyCommand が HTTP(S) proxy を使う

たぶん私はやりません。誰かやって。

Tips (1) - Killed by signal 1 がうざい

これまでの多段 ssh の例で,リモートからログアウトすると,以下のようなメッセージが出力されます。

target% exit

Connection to target closed.
Killed by signal 1.

% ■

signal 1 というのは SIGHUP のことです。詳しい原因についてはうまく説明できないのですが,おそらく以下のような原因だと思います。

(先ほどの図例で説明すると)host:local の [ssh A] と host:target の [sshd D] の通信が先に終了し,[ssh B] のプロセスが終了する前に [ssh A] が終了してしまう。[ssh A][ssh B] 間の tty 接続が切れ,そのことによって [ssh B]SIGHUP が発生している,と。

ともかく,ProxyCommand の先頭に nohup というコマンドを挿入しておくとこのメッセージを suppress することができます。

% ssh -o 'ProxyCommand nohup ssh gateway nc %h %p' target

nohup: redirecting stderr to stdout

target% 

target% exit

% ■

そのかわり,接続開始時にメッセージが出るようになってしまいましたけどね。

Tips (2) - ssh gateway ssh target じゃだめなの?

単純に実行すると(面倒なので ssh-agent forward 有効でテストしますが),

% ssh gateway ssh target

Pseudo-terminal will not be allocated because stdin is not a terminal.
■

このようにエラーが出力されます。でも実は接続じたいは継続しています。

% ssh gateway ssh target

Pseudo-terminal will not be allocated because stdin is not a terminal.

hostname ← ユーザの入力
target
exit     ← ユーザの入力

% ■

プロンプトは出力されませんが,接続としては成されていることがおわかりいただけたかと思います。メッセージにかかれているとおり,仮想端末(pty)がアロケートされていないのでプロンプトや行バッファなどの機能が無効になっているのです。

原因と対策については sshで多段ログイン - 技術メモ帳 に記述がありますが,-t を指定すればよいです。

% ssh -t gateway ssh target

Last login: Thu Feb  7 19:02:06 2008 from gateway
target% ■

無事シェルとして使えるようになりました。


ssh-agent forward が有効ではない場合,-t オプションなしでは password auth や publickey auth がうまくいきませんが,-t オプションをつけると password / passphrase を入力することができ,ログインできます。

% ssh gateway ssh target
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********

Pseudo-terminal will not be allocated because stdin is not a terminal.
Permission denied, please try again.
Permission denied, please try again.
Permission denied (publickey,gssapi-with-mic,password).

% ←ログインできなかった

% ssh -t gateway ssh target
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********
Enter passphrase for key '/home/dayflower/.ssh/id_dsa': ********

Last login: Fri Feb  8 12:17:36 2008 from gateway
target% ←ログインできた


これらの接続の図を下記に示します。

→[ssh] Local   12345 =→ Gateway [sshd]→[ssh] Gateway 23456 =→ Target  [sshd]ー→[bash]
←[ A ] Machine ←===22 Machine [ C  ]←[ B ] Machine ←===22 Machine [ D  ]←ー[    ]

この図からわかるとおり,

  • [ssh A] ←→ [sshd C][ssh B] ←→ [sshd D] は独立して接続・暗号化している
  • [ssh A][sshd D] が直接接続しているわけではない
  • したがって,公開鍵認証の場合,以下のいずれかの条件がみたされている必要がある


以上から,

自動運転などの場合,

  • 運転元に秘密鍵を集約しておき,ProxyCommand を利用して多段アクセスする
  • 設定ファイル(秘密鍵ssh_config)はすべて運転元においておくことができる
  • 中継ホストの authorized_keys は command="nc target 22" とか書いておくといいのかな…未確認
    • それでもなんだかやな感じですね

個人ユーザの場合,

  • あちこちにログインする場合,あちこちに自分の公開鍵だけ登録しておく
  • メイン端末で ssh-agent を立ち上げ,ssh-agent forwarding を有効にする
  • 多段の場合,いっこずつログインしていくか,-t オプションを使って一気にログインする
  • ただし,ssh-agent, ssh-agent forwarding, keychain 等,便利なしくみを利用すればするほどリスクは増える
  • もちろん個人ユーザでも多段の場合 ProxyCommand 手法を使ってもよい
    • デメリットは……なんだろ。(遠くのホストが増える度に)ssh_config の内容を編集していく必要があること?

*1:推測ですが

*2:agent が forward されているわけでもありません