多段 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 %pssh_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
こんなイメージです。
私の使用している RedHat 系 Fedora / CentOS では nc がインストールされていました。netcat には様々な実装が存在するのですが,
- *Hobbit* 氏が 1996 年に初代 netcat をリリース
- OpenBSD のユーザランドコマンドとして nc (netcat) が取り込まれる
- SOCKS 5, SOCK 4, HTTPS proxy 機能等が追加される ⇒ CVS log for src/usr.bin/nc/netcat.c
- RedHat が自社 OS に OpenBSD 由来の nc をパッケージとして取り込む
という経緯で添付されているようです。
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
目的は以下のとおりです。
ここで要求される条件は,
- 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 ]←ー[ ]
ポイントは,
- [ssh B] ←→ [sshd C] 間と,[ssh A] ←→ [sshd D] 間の接続は,それぞれ暗号化されている
- [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秒以上通信がなかった場合に接続が切れるようにしてあります。
高度な例 (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] が直接接続しているわけではない
- したがって,公開鍵認証の場合,以下のいずれかの条件がみたされている必要がある
- host:gateway に host:target のアカウントの秘密鍵をおいておく
- host:local で ssh-agent が立ち上がっており,ssh-agent forwarding が有効になっている(⇒ An Illustrated Guide to SSH Agent Forwarding)
以上から,
自動運転などの場合,
- 運転元に秘密鍵を集約しておき,
ProxyCommand
を利用して多段アクセスする - 設定ファイル(秘密鍵や
ssh_config
)はすべて運転元においておくことができる - 中継ホストの authorized_keys は command="nc target 22" とか書いておくといいのかな…未確認
- それでもなんだかやな感じですね
個人ユーザの場合,