複数ホストに対してSSH越しにsudoを実行する
はじめに
表題の通りに複数ホストに対してsudoコマンドを実行したいことがありました。
このようなケースの場合は以下のような手段を取るのが普通だと思います。
- sudoを実行するホストのsudoersを編集する
- fabricやansibleなどのサードパーティツールを利用する
- tmuxなどを利用して画面分割し同時にコマンドを実行
今回は接続先のホストをsudoersを勝手に修正することができず[1]、また接続元のホストもインターネットに接続されておらずセキュリティポリシーの都合からサードパーティ製のツールも使えませんでした。なので今回はsshとbashだけでこの問題を解決してみました。
パスワードなしでsudoを実行する方法
sshした時にパスワードの入力を求められないようするにはどうしたらよいのでしょうか?とりあえずman(8)で調べてみると、以下のオプションが見つかりました。
-S, --stdin
プロンプトを標準エラーに表示するが、パスワードの読み込みは、 ターミナルデバイスを使わずに、標準入力から行う。パスワードは、 末尾に改行を付けなければならない。
なので以下のコマンドで事前にsudoで必要なパスワードを標準入力から渡すことができます。
とりあえずローカル環境で試してみます。
$ read -sp "sudo password: " _password
sudo password:
sudoパスワードをそのままechoで渡してしまうと、bash_historyにパスワードが残ってしまいます。そのため、ここでは、readコマンドを使ってプロンプトからパスワードを取得し、それを_password
変数に格納するようにしています。
$ echo $_password | sudo -S sudo echo OK
[sudo] password for worbridg: OK
そしてsudo echo OK
を実行する時に[sudo] password for worbridg
でパスワードは求められましたが、標準からパスワードを渡しているので、そのままコマンドを実行することができました。
$ unset _password
最後にunset _password
を実行しているのは、このまま放置するとパスワードが_password
変数に残ったままになるのでunsetで消してあげます。
sshでsudoを実行するときも同様の操作でうまくいきそうです。
brace expansion を利用する
複数のホストに対してsshを実行するためにまずホストの一覧をリストアップしてあげる必要があります。ホスト名一覧をファイルに作成して、それをforやxargsなどでループしてsshしてあげても良いですが、ホスト名一覧ファイルを作成するのも億劫なので今回はbrace expasionを利用します。
brace expasionは..
を使うことで下記のように数値または英字を特定の範囲で展開することができます。私が今回sudoを実行しようと思っていた接続先ホスト名がちょうど連番だったのでbrace expasionがうまく活用できそうです。
$ echo 1{1..3}{a..c}
11a 11b 11c 12a 12b 12c 13a 13b 13c
完成
以上の点を踏まえて、簡単なシェルスクリプトを作ってみました。
複数ホストに対してsshしてコマンドを実行するとので、mssh(multi ssh)という適当な名前の関数を用意しました。これを.bashrc
などに書いておけば利用できるようになります。
完成形は以下の通りです。
function mssh {
if [ $# -lt 2 ]; then
cat <<EOS
usage: mssh <command> [<host>,...]
Example:
$ mssh "sudo echo OK" server01
or
$ mssh "sudo echo OK" server0{1..3}
EOS
return 1
fi
local command="$1"
# 毎回sudoパスワードを入力するのは面倒
# コマンドにsudoが含まれている場合のみパスワード入力を求めるようにする
if [[ "$command" == *sudo\ * ]]; then
read -sp "sudo password: " _password
echo
fi
local hosts=${@:2}
for host in $hosts; do
echo -n "$host: "
if [ "$_password" = "" ]; then
ssh $host "bash -c \"$command\""
else
# 本記事のコメントにて指摘があったので修正しました!
# ssh $host "echo $_password | sudo -S bash -c \"$command\""
echo $_password | ssh $host "sudo -S bash -c \"$command\""
fi
done
unset _password
}
実行すると次のような結果になります。
$ mssh "sudo hostname" server0{1..3}
sudo password:
server01: [sudo] password for worbridg: server01
server02: [sudo] password for worbridg: server02
server03: [sudo] password for worbridg: server03
$ mssh hostname server0{1..3}
server01: server01
server02: server02
よく見るとecho $_password | ...
としているのでパスワードがbash_historyに残ってしまうのでは?っと思いましたが、こちらの回答を見てみると
シェルのヒストリーは対話モードかつ端末から入力されたコマンドラインのみを記録します。
っとあるのでパスワードがbash_historyに残ることは無いと思います。
追記
そもそもssh先でecho $_password
しなくてもローカルから実行してもパスワードをリモート先のbashに渡せるのでコードは修正しました。@ttdodaさん、ありがとうございます!
おわりに
閉じられた環境でも頑張れば、複数ホストにsshできました。
但し、今回作成したスクリプトはCtrl-Cすれば_password
変数がunsetされなかったり、sshで実行するコマンドの内容を検査していないのでコマンドインジェクションが起きうるの危険性があります。なのであくまでこの方法は個人利用にしておくのが良いと思います。これ以上に高度な作業をしたい場合はおとなしくfabricやansibleなどを使う方が良いと思います。
-
申請すれば可能ではありました ↩︎
Discussion
これだとパスワードの内容を
ssh
コマンドに渡して、サーバ側でecho
が実行されますよね。それだと
<
,>
,$
,"
等のシェルで特別な意味を持つ文字が含まれていると誤動作するps
コマンドを実行するとパスワードが見えてしまう (ローカル, サーバ側両方)などの問題があります。
それよりは
のように
echo
コマンドをローカル側で実行する方が前述の問題が出ないのでいいと思います。指摘ありがとうございます!
ローカルから
echo
でパスワードを渡しても動作できるなら、そちらの方が良いですね!記事の方も訂正しておきます