sudo clushでUbuntuクラスターを管理する

Ubuntuなど最近のLinuxではroot(スーパーユーザー)でのログインを基本的に行わせず、かつrootのみ実行可能な管理コマンドはsudoを通して使わせる思想のものが多い。このrootユーザーを直接的に使わない思想を守りつつ、クラスターを構成する多数のノードを ClusterShell (clush) で管理する方法を調べてみた。その結果行き着いた自分なりのベストな解は「sudoclushで投げる代わりにclushsudoする」。たとえば次のように管理コマンドを投げられるようにする:

sudo clush -w worker[1-3] shutdown -r now

以下、これを可能にするためのセットアップ手順などについて書いていく。なお話を簡単にするため、以下ではクラスターを管理するためのノードをmanager、そこから管理したいノードをworkerとする。またworkerは合計3台あり、それぞれ名前はworkerN(Nの部分は1~3)とする。

前提

この記事はUbuntu Server 16.04で動作検証をしながら書いた。とはいえ別段Ubuntu固有の話でもないため、rootが無効化されている他のLinuxディストリビューションでも同様に使えると思う。

この記事では以下3点をカバーしている:

  • 公開鍵認証方式でのパスワードレスsshの実現方法
  • パスワード未設定のrootユーザーでパスワードレスsshを可能にする方法
  • sudo clushで複数のリモートホストで一斉に管理コマンドを実行可能にする方法

まえがき

ClusterShell (clush)とは

ClusterShell (clush) というツールは、複数のLinuxサーバーに対して一斉にssh接続とコマンド投入を行う。特に、スクリプト化できない非定型的な操作が必要なときにはコマンド単位で多数のノードを一斉操作できるclushが大いに活躍する。

clushとsudoの問題

rootのみ実行可能なコマンド(以下「管理コマンド」)をリモートで実行したいとしよう。CentOSのようにrootアカウントでのログインを認める思想の(OSインストール時にrootのパスワードを設定してログイン可能にする)Linuxであれば、単純にmanagerのrootアカウントでclushを実行すればworkerでもrootでコマンドが実行されるため特に悩むことは無い。しかしworkerのOSがUbuntuであって、それらに対して一斉に管理コマンドを投げ込む場合、素直に考えるとsudoコマンドを投げることになる。しかしsudoは「パスワード入力を求めてくる」ため、sshあるいはclushでリモート実行すると3つほど面倒事が出てくる:

  1. tty割り当てオプションが毎回必要で手間
  2. 毎回パスワードを聞かれて手間
  3. 自動化しにくい(パスワードを平文でスクリプトに書きたくない)

まず1点目。ssh越しで実行されたリモートコマンドにはttyが割り当てられないらしく、標準入力の受け付け状態に入ってくれない。そのため何も考えずにsudoを投げるとsudo: no tty present and no askpass program specifiedといったエラーがリモーsトから返ってきて失敗してしまう。この問題についてはsshコマンドの-tt-t二つでtty割り当てを強制)オプションを使うと解決する。clushの場合、-oオプションにsshコマンド用オプションを指定すれば良い(例: clush -o -tt -w hostname sudo shutdown -r now)。ただ、毎回オプションを付けるのは手間だし、パイプでclushの標準入力を通してリモートコマンドにデータを流し込みにくくなる(パスワード、データと連続して流し込む必要が出てくる)デメリットもある。できれば、回避したい面倒事である。なおsshpassというツールを使う手もあるけれど、セキュアではない*1で個人的にはオススメしない。

続いて2点目。あるホストにログインした状態であれば、一度sudoを実行した後しばらくの間はパスワード入力を省いてくれる。これはsudoを使う気にさせる(rootを有効化してrootでログインしたいと思わせない)ために重要なポイントだと思っている。しかしsshログイン・コマンド実行・ログアウトをworkerに対して一斉実行するclushでsudoを含むコマンドラインを実行すると、それは各workerにとって常に「ログイン後初めて実行されるsudo」となる。そのため、どんなに短い間隔でclush sudoを実行したとしても、毎回パスワード入力を求められることになる。これはインタラクティブクラスターを操作していると大きな手間であり、やはり回避したい面倒事だ。

最後に3点目。パスワード入力が必要になるということは、cron等で完全自動実行されるスクリプトまで考えると「どこかに復元可能な形でパスワードを記録しておかなくてはならない」ということだ。したがってパスワード保護*2とパスワード更新管理という頭痛の種が出てくるため、やはり回避したいところだ。

以上の3点の面倒事を、rootアカウントを有効化せずに解消するため、sudoを使う管理コマンドをclushで投げる代わりに「管理コマンドを投げるclushをsudo」できるよう環境構築してみた。

解決策

まず背景知識として、以下のポイントがある。

  • sudoでsshコマンドを実行するとrootユーザーでログインする
  • Ubuntuのrootは「パスワードが設定されていない」だけ

1つ目は、sudoはコマンドをrootユーザーとして実行するものだという点。そのためsshコマンドをsudoで実行するとrootユーザーとしてリモートにログインすることになる。

2つ目は、Ubuntuのrootは存在しないわけではなくパスワードが設定されていないだけである点。そのためパスワードを設定すれば普通にログインできるようになるし、公開鍵認証でのパスワードレスsshをセットアップすればパスワードの無い状態でもログインできるようになる。

つまり、公開鍵を使ったパスワードレスsshを行えるようmanagerのrootユーザーに設定をしておけば、sudo sshでパスワード未設定なrootアカウントを使ったsshログインが可能になる。

セットアップ手順

概要

セットアップ手順は、おおよそ以下5ステップとなる。

  1. 各workerのホスト鍵のフィンガープリント値をメモ
  2. clushをインストール
  3. 普段使いの管理者ユーザーでパスワードレスsshを可能にする
  4. managerのrootの認証鍵を生成し、workerのrootのauthorized_keysに登録
  5. workerのホスト鍵をmanagerのrootのknown_hostsに登録

本質的には3ステップ目で行うパスワードレスsshのセットアップをrootに対しても行えば良い。ただ、rootにはパスワードが無いため公開鍵を転送・登録するまでssh接続ができず、したがってssh-copy-idコマンドなどの便利なコマンドも使えない。そこで普段使いの管理者ユーザーで鍵の登録操作などを代理(?)実行してセットアップしていく。

workerのホスト鍵のフィンガープリント値をメモ

まずworkerごとに物理ログインしてホスト鍵のフィンガープリント値を表示してメモし、ホスト名とフィンガープリント値の対応表を作っておく。暗号アルゴリズムRSAを使用する場合、以下のコマンドでホスト鍵のフィンガープリント値を表示できる:

$ ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key.pub
2048 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxx root@worker1 (RSA)

RSAでない暗号アルゴリズムを使う場合はファイル名が異なる。指定すべきファイル名はman sshdで確認されたい(だいたい予想できると思うけれど)。

なお、どうしてもフィンガープリント値をメモするのが面倒ということであれば先頭・末尾の数文字だけでもメモしておいて欲しい。それだけを照合するだけでも、多少は効果があるので。

clushをインストールする

clushはUbuntu Serverの11.04 (Natty)以降であればaptでインストールできる:

sudo apt install clustershell

Python使いであればpip install clustershellでインストールしても良いと思う。clushのバージョンを自由に選べるメリットもある。なおaptでインストールされるバージョンは、Ubuntu 16.04では1.7(1.7系の初版)、Ubuntu 18.04では1.8だった(2018年6月10日の時点)。

なお、この記事はaptでインストールしたclush 1.7で動作確認しつつ書いている。

普段使いの管理者ユーザーでパスワードレスsshを可能にする

まず普段使い管理者ユーザーでmanagerにログインし、認証用の鍵を生成していなければ生成する(生成済みであれば以下コマンド実行時に「上書きするか」と聞かれる)。

ssh-keygen -t rsa -P "" -f ~/.ssh/id_rsa

続いてssh-copy-idコマンドでworkerの同ユーザーの認可済み鍵一覧 (authorized_keys) に、生成した公開鍵を登録する。

ここで一つだけ注意点を。sshコマンドは過去に接続したことの無いホストに接続すると、そのホストのホスト鍵のフィンガープリント値を表示して「信頼して良いか」と尋ねてくる。これに対してyesと入力する前に、最初の手順でメモしておいたフィンガープリント値と一致することを、必ず目視確認されたい。さもなければセキュリティリスクを負うことになる(理由は後述)。

話を戻そう。以下のようなコマンドで、公開鍵をworkerに登録できる。なおこのコマンドは連続してsshログインを実行していくため、都度パスワードを入力(初回接続時はホスト鍵フィンガープリント値の確認も)する必要がある。

for node in $(nodeset -e worker[1-3]); do ssh-copy-id $node; done

このnodesetというコマンドは、clushと一緒にインストールされるもの。nodesetの-eオプションはclushの-wオプション、つまり接続先ホストを指定する際に使うパターン文字列を展開してくれる。なおnodesetコマンドはホスト名のパターン文字列を展開するだけでなく、逆に複数のホスト名を元にパターン文字列を生成する機能もある(例: nodeset -f worker1 worker2を実行するとworker[1-2]と表示される)。clushでの運用を行うにあたってはパターン文字列を必ず使うことになるので、nodesetというコマンドの存在は覚えておきたいところ。

さて、workerへのssh-copy-idが成功したら、今度は全workerに対しパスワードレスsshができるようになったか確認しておくと良い。たとえば以下のようにclushでhostnameコマンドを一斉投入し、エラーが出なければOKだ。

clush -w worker[1-3] hostname

managerのrootの認証鍵を生成し、workerのrootのauthorized_keysに登録

今度は、パスワード未設定のため普通には使えないrootユーザーで公開鍵認証できるよう設定する。まずmanagerにて、sudoを使って「rootユーザーの」認証鍵ペアを生成する。

sudo ssh-keygen -t rsa -P "" -f /root/.ssh/id_rsa

続いて生成したrootユーザーの公開鍵を、workerにclushで一斉にコピーする。ここでは普段使いの管理者ユーザーのホームディレクトリにいったん仮置きしている。

sudo cp /root/.ssh/id_rsa.pub ~
clush -w worker[1-3] --copy ~/id_rsa.pub --dest ~  # --destは省略しても良い

その後、workerにsudoで「公開鍵をrootユーザーのauthorized_keysに追記」するコマンドラインを投入する($PASSWORDの部分はsudoに渡すパスワードに置き換えて実行すること)。

echo $PASSWORD | clush -w worker[1-3] -o "-tt" sudo mkdir -p /root/.ssh
echo $PASSWORD | clush -w worker[1-3] -o "-tt" "cat ~/id_rsa.pub | sudo tee -a /root/.ssh/authorized_keys"

なおリダイレクト等を含むコマンドラインをリモートに投げる場合、リモートで実行したいコマンドライン全体を引用符で囲う必要がある。さもなければローカルのシェルでリダイレクト等が解釈されてしまい、意図した動作にならないので注意されたい。

最後に、authorized_keysのオーナー・パーミッションを設定する。

echo $PASSWORD | clush -w worker[1-3] -o "-tt" sudo chown root:root /root/.ssh/authorized_keys
echo $PASSWORD | clush -w worker[1-3] -o "-tt" sudo chmod 600 /root/.ssh/authorized_keys

以上で、worker側ではrootユーザーによるパスワードレスsshを受け入れ可能になる。なお作業で一時的に作成した~/id_rsa.pubは、もう削除して良い。

rm ~/id_rsa.pub                       # manager上の一時ファイルを削除
clush -w worker[1-3] rm ~/id_rsa.pub  # worker上の一時ファイルを削除

workerのホスト鍵をmanagerのrootのknown_hostsに登録

最後の仕上げに、managerのrootユーザー用known_hosts(既知の信頼済みホスト一覧)にすべてのworkerを追加する。

まず、managerから全workerのホスト鍵のフィンガープリントをネットワーク越しに収集し、ファイルに保存する(rsa以外のアルゴリズムを使いたい場合は適宜指定を変更すること)。

ssh-keyscan -H -t rsa $(nodeset -e worker[1-3]) > ~/known_hosts

続いて、収集したフィンガープリント値をknown_hostsに追加…する前に、あらかじめメモしておいた各workerのホスト鍵フィンガープリント値と、ネットワーク越しに収集したものが一致していることを目視確認する(理由は後述)。具体的には、以下のコマンドで収集したフィンガープリント値を表示させ、メモと読み比べる。

for node in $(nodeset -e worker[1-3]); do ssh-keygen -l -f ~/known_hosts -F $node; done

すべてのフィンガープリント値が期待通りであった場合、これらのフィンガープリント値をmanagerのrootユーザーのknown_hostsに追加する。

cat ~/known_hosts | sudo tee -a /root/.ssh/known_hosts
sudo chown root:root /root/.ssh/known_hosts
sudo chmod 644 /root/.ssh/known_hosts

以上で、managerのrootユーザーからworkerに対してパスワードレスsshが可能となり、したがってsudo clushでworkerへ一斉に管理コマンドを投入可能になる。

最後に、一時的に作成した作業ファイルを削除して作業完了だ。

rm ~/known_hosts                       # manager上の一時ファイルを削除
clush -w worker[1-3] rm ~/known_hosts  # worker上の一時ファイルを削除

まとめ

  • clushでsudoをリモート実行すると何かと面倒なので避けるべき
  • 正しく設定すればsudoを通してrootでのパスワードレスsshが可能
  • 設定完了後はsudo clushでリモートに管理コマンドを一斉投入可能になる

補足情報

ホスト鍵フィンガープリント値を目視確認する理由

ネットワーク越しに取得したフィンガープリント値をいきなり信用(known_hostsに追記)してはならない理由を、セキュリティ関連の話なのでマジメに補足しておく。

まずネットワーク越しで取得したとき、当然ながらホスト名やIPアドレスで相手を探索し、接続している。ここで、この時点では接続先ホストのフィンガープリント値すなわち「信頼できるホストかどうかを確認するための値」が不明なまま接続し、その値を尋ねた格好になっている。これを人間社会で例えるなら、見知らぬ人間に身分証を出させた状態と同じだ。ならば、その相手が別人になりすましたスパイである可能性が少しでもあるならば、何も考えずにその身分証を信用してはならない。この例え話と同様に、フィンガープリント値という身分証を提示したホストがIPアドレス偽装などを行った悪意あるホストである(中間者攻撃を受けている)可能性が少しでもあるならば、そのフィンガープリント値を何も考えずに信用してはならない。

こうした背景があるため、各ホストに物理的にログインして確認した値と同一であると確認できたフィンガープリント値のみを信頼しなければ危険…という結論になる。なお、クローズネットワークで環境構築中である等、確実に中間者攻撃等のリスクが無いと判断できるなら厳密にチェックする必要は無い。逆に言うと、構成ノード数が膨大で一つ一つに物理ログインして操作などできないような規模のクラスターをセキュアにセットアップするならば、まずクローズネットワークでこうした管理用の設定を済ませておき、それが終わってから外部接続するべきなのだろう。

表示結果を見やすくするには

clushの標準動作ではリモートで実行されたコマンドの表示内容を逐一表示していくので、出力順序が実行するたびに変わってしまう。また、「全ノードで同じ結果が表示されるか」を確認するにはノードの数だけ表示されるメッセージを目視で比較する必要があり、大変だ。そこで、リモートでのコマンド実行結果を確認する際に便利な表示系オプションを3つ紹介しておく。

  • -L … 出力結果を一行ずつ、ただしノード単位でソートする
$ clush -L -w worker[1-3] "ls -d .bash_*"
worker1: .bash_history
worker1: .bash_logout
worker2: .bash_logout
worker3: .bash_history
worker3: .bash_logout
  • -B … 出力結果が同一だったノードをまとめる
$ clush -B -w worker[1-3] date
---------------
worker[1,3] (2)
---------------
Sun Jun 17 17:31:12 JST 2018
---------------
worker2
---------------
Sun Jun 17 17:28:37 JST 2018
  • --diff … 出力結果の違いをunidiff書式で表示
$ clush -B -w worker[1-3] cat /etc/hosts
--- worker[1,3] (2)
--- worker2
@@ -7,6 +7,6 @@
 
 
 192.168.99.10   master
-192.168.99.11   worker1
+192.168.99.11   workar1
 192.168.99.12   worker2
 192.168.99.13   worker3

ログイン失敗する場合

sshdは公開鍵認証を許可しているか?

worker(ssh接続を受ける側)のsshdが公開鍵認証を許可しているか確認して欲しい。sshd_configにてPubkeyAuthenticationyesになっていれば許可できている。

$ grep ^PubkeyAuthentication /etc/ssh/sshd_config
PubkeyAuthentication yes

sshdはrootログインを許可しているか?

worker(ssh接続を受ける側)のsshdがrootでのログインを許可しているか確認して欲しい。sshd_configにてPermitRootLoginyesprohibit-password、またはwithout-passwordのいずれかになっていれば許可できている。

$ grep ^PermitRootLogin /etc/ssh/sshd_config
PermitRootLogin prohibit-password

sshクライアントは公開鍵認証を許可しているか?

manager(ssh接続を仕掛ける側)のsshコマンドが公開鍵認証を許可しているか確認して欲しい。ssh_configにてPubkeyAuthenticationyesになっていれば許可できている。

$ grep ^PubkeyAuthentication ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null

何も表示されないか、yesと表示されば許可できている。なおssh_configはユーザー共通の設定ファイルとユーザー固有の設定ファイルがあるため、両方を確認する必要がある。また設定が齟齬していた場合、後者が優先される。

以上。