🐙

複数ホストに対してSSH越しにsudoを実行する

2021/12/01に公開
2

はじめに

表題の通りに複数ホストに対して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などを使う方が良いと思います。

脚注
  1. 申請すれば可能ではありました ↩︎

Discussion

いわもと こういちいわもと こういち
ssh $host "echo $_password | sudo -S bash -c \"$command\""

これだとパスワードの内容をsshコマンドに渡して、サーバ側でechoが実行されますよね。
それだと

  • パスワードに <, >, $, " 等のシェルで特別な意味を持つ文字が含まれていると誤動作する
  • 実行するコマンドが時間がかかる場合、実行中に他ユーザがpsコマンドを実行するとパスワードが見えてしまう (ローカル, サーバ側両方)

などの問題があります。

それよりは

echo $_password | ssh $host "sudo -S bash -c \"$command\""

のようにechoコマンドをローカル側で実行する方が前述の問題が出ないのでいいと思います。

worbridgworbridg

指摘ありがとうございます!
ローカルからechoでパスワードを渡しても動作できるなら、そちらの方が良いですね!
記事の方も訂正しておきます