セキュアな rsync - 実践編

理論編(id:JULY:20111127)を書いてから、既に1年半も経ってしまいましたが*1、ようやく実践編です。

2台のホストは

ホスト名
コピー元 master.example.com
コピー先 slave.example.com

とします。SSH は slave.example.com から master.example.com へ接続します。

rsync 用のユーザの作成

双方のホスト上で rsync 用のユーザを作ります。このユーザは普通にログインする必要が無いので、ログインシェルを /sbin/nologin にしておきます(後述しますが、とりあえずこの時点では、ログインシェルは /sbin/nologin としておきます)ホームディレクトリも必要ありません。

あとは基本的には任意ですが、多くのディストリビューションで 500 番以上の UID を一般ユーザに割り当てる習慣があるので、500 番未満が良いでしょう*2

# groupadd -g 480 rsync
# useradd -d / -M -u 480 -s /sbin/nologin -g rsync rsync

ssh のホストベース認証

これについて書いているサイトは多いので、省略... でも良いのですが、一応、書いてみます。

鍵の用意

多くの Linux ディストリビューションでは、最初に sshd が起動された時に生成するようになってます。/etc/ssh の下に、下記のようなファイルがあると思います。

秘密鍵 公開鍵 対応プロトコルバージョン
ssh_host_key ssh_host_key.pub Version 1
ssh_host_dsa_key ssh_host_dsa_key.pub Version 2
ssh_host_rsa_key ssh_host_rsa_key.pub Version 2

今時、SSH1 のプロトコルを使う必要は無いので、「ssh_host_dsa_key、ssh_host_dsa_key.pub」か「ssh_host_rsa_key、ssh_host_rsa_key.pub」のどちらかを使う事になります。違いは公開鍵暗号として、DSA を使うか、RSA を使うか、ということですが、現在は RSA を使うのが一般的です*3

これらの生成済みのファイルは、「このホストを認証してもらうための鍵」です。今回の例では、slave.example.com 側のファイルです。

もし無ければ*4ssh-keygen で生成します。

$ ssh-keygen -f ssh_host_rsa_key -t rsa -N ''
Generating public/private rsa key pair.
Your identification has been saved in ssh_host_rsa_key.
Your public key has been saved in ssh_host_rsa_key.pub.
The key fingerprint is:
......

-f は出力するファイル名で、このファイル名で秘密鍵が生成されます。このファイル名に「.pub」が付いた名前で、対応する公開鍵のファイルが保存されます。-t は公開鍵暗号の暗号方式で rsa をして、-N はパスフレーズの指定で、「-N ''」とする事で、パスフレーズ無しの鍵が作られます*5

鍵の配備

今回は、slave.example.com が、master.example.com認証してもらう立場なので、slave.example.com のホスト公開鍵を、master.example.com にコピーします。

認証する側では、/etc/ssh/ssh_known_hosts ファイルに公開鍵の内容を追記します。認証される側で ssh サーバが動いているのであれば、認証する側のホスト上で下記のようにすると、/etc/ssh_known_hosts ファイルに公開鍵を追記できます。

# ssh-keyscan slave.example.com >>/etc/ssh/ssh_known_hosts
ホストとユーザの制限

認証する側の/etc/ssh/shosts.equiv で、許可するホストとユーザ名を指定できます。ただし、sshd_config の次の項目を確認する必要があります。

IgnoreRhosts
yes にすると、ユーザのホームディレクトリにある .shost ファイルを無視します。これが no になっていると、/etc/ssh/shosts.equiv に記述が無くても、エンドユーザレベルで許可されている可能性があります。

今回は master.example.com の管理者による制限を行いたいので、IgnoreRhosts は yes にします*6

shosts.equiv の記述自体は簡単で、認証を許可するホスト名とユーザ名を記述します。ユーザ名は省略可能で、その場合は記述したホストの全てのユーザを許可します*7。slave.example.com から master.example.com へ接続するユーザを user1 とすると、shosts.equiv の内容は以下のようになります。

slave.example.com user1

しかし、今回の場合、

ユーザ ホスト ユーザ ホスト
rsync slave.example.com rsync master.example.com

という接続になります。shosts.equiv に書くのは、あくまでも、認証してもらう側のホスト上のユーザなので、

slave.example.com rsync

となります。

sshd_config の設定

先述の IgnoreRhosts に加えて、残っているのは、そもそもホストベース認証を有効にするための設定です。

HostbasedAuthentication
ホストベース認証の有効/無効を設定します。これと似たものに RhostsRSAAuthentication がありますが、これは SSHプロトコルバージョン 1 用の設定で、プロトコルバージョン 2 で使われるのは、この HostbasedAuthentication です。

ssh_config の設定

ssh クライアント側の設定で必要なのは、下記の2項目です。

HostbasedAuthentication
ホストベース認証の有効/無効を設定します。ここは sshd_config の時と同様です。
EnableSSHKeysign
ホストベース認証時にクライアントサイドで動く補助プログラム ssh-keysign を有効にします。

ssh_config は、Host キーワードを使って、特定の接続先にだけオプションを適用する事ができます。ssh のサーバになるホスト名をして、

Host master.example.com
    HostbasedAuthentication yes

とすれば、ホストベース認証を使う相手先を master.example.com だけに絞る事ができます。ただしEnableSSHKeysign は、この Host キーワードによる指定の外側に記述する必要があります。

EnableSSHKeysign yes

Host master.example.com
    HostbasedAuthentication yes

sudo の設定

rsync コマンドを cron に設定して実行する側は、

  • 一般ユーザの cron で実行する。
  • 面倒だし、分かりづらくなるので、/etc/cron.daily とかに設定してしまう。

の2つの方法が考えられます。後者であれば、rsync をスケジュール起動する側(ここでは、slave.example.com)では、sudo の設定は不要です。

前者であれば、/etc/sudoers で、rsync を実行する時のユーザで、rsync を root 権限で実行できるようにする必要があります。仮にその時のユーザアカウントが rsync-user であれば、

rsync-user localhost=(root) NOPASSWD:/usr/bin/rsync

と記述し、後述の requiretty に関する記述を追加した上で、「sudo rsync 〜」と、sudo 経由で rsync を実行します。

逆に、ssh 経由で rsync を呼び出される方は sudo の設定は必須です*8

rsync ALL=(root) NOPASSWD:/usr/bin/rsync

これで、rsync を root 権限で呼び出せるようにはなったのですが、多くのディストリビューションの /etc/sudoers で、

Defaults    requiretty

という行が入っています。これは、sudo を使ってコマンドを呼び出す際、端末としてログインしている状態である事を要求します。リモート側で実行される sudo rsync は、ログインセッションでは無いので、除外してやる必要があります。ここで、

Defaults    !requiretty

としたり、コメントアウトする例を見かけますが、これだと、全ての sudo の実行で、ログインセッション以外での sudo 実行を許可する事になります。rsync ユーザだけ、ログインセッション以外での実行を許可するためには、

Defaults:rsync  !requiretty

という行を追加します。

rsyncコマンドライン

ポイントは2つです。

通信を、sudo 付きの ssh 経由にする

「-e」オプションで ssh を指定するのですが、その際、「-e 'sudo -u rsync ssh -l rsync'」と、sudo 経由で、かつ、ユーザ名付きにする必要があります。こうすることで、

  • ローカル側の rsync が呼び出す sshrsync ユーザで実行する。
  • 呼び出された ssh は、接続先の rsync ユーザに対して繋ぎに行く。

という動作になります*9。この時の「-u」で指定するユーザ名は、master.example.com の shosts.equiv で指定したユーザ名と合致している必要があります。今回は、ssh を実行するユーザ(-u rsync)と、接続先のユーザ(-l rsync)が同じなので、「-l rsync」は省略可能です。

リモート側で実行される rsync を sudo 付きにする。

「--rsync-path='sudo rsync'」とします。これを忘れると、リモート側の rsync は「rsync アカウント権限で rsync を実行」する事になり、読み出しに root 権限が必要なファイルが取得できなくなります。

全体としては、こんな感じのコマンドラインになります。

rsync -a -e 'sudo -u rsync ssh -l rsync' --rsync-path='sudo rsync' master.example.com:コピー元 コピー先

非 root ユーザでこれを実行する場合は、この手前に sudo を付けます。

コピー元の rsync ユーザの制限

実は、動作を確認している中で、ここでつまずきました。目指す rsync の実行は、

  1. slave.example.com 側で rsync を実行。
  2. その rsync が「ssh -l rsync」として ssh を実行。
  3. 繋がれた master.example.com では「sudo rsync 〜」を実行。

となります。冒頭、rsync ユーザのログインシェルを「/sbin/nologin」としていますが、そうするとすると、ssh でつながった直後に切断され、3 番目の「sudo rsync 〜」を実行できなくななります。

なので、普通の「/bin/bash」などをログインシェルにしてしまえば、これまでの設定で目的が果たせます。

... ただ、なんとかしたい。rsync 実行の為だけに用意したアカウントで、slave.example.com から繋いだ時だけとは言え、rsync ユーザに許可されている任意のプログラムが実行できる、というのは、どうにも格好悪い。rssh を使えば、scp や sftp、rsync 用に絞ることも可能だけど、今回は sudo 経由で rsync を呼び出すので、これは使えない...。

で、あれこれ調べまわった挙句*10、ログインシェルを自作しました*11

#!/bin/sh
if [ "$1" != "-c" ]; then
    exit 0
fi

if (expr "$2" : 'sudo  *rsync  *--server  *--sender ' >/dev/null); then
    echo -n "$2" | egrep -q '[^\][;<>]|&&|\|\|'
    if [ $? -eq 0 ]; then
        exit 0
    fi
    /bin/sh "$@"
else
    exit 0
fi

ssh 経由でリモート側で呼び出される rsync コマンドには「--server」というオプション付きで呼び出されます。で、今回のケースのように、リモート側からローカル側へダウンロードする場合、「--sender」というオプションも付きます。

これらのオプション付きの rsync が sudo 経由で呼ばれ、かつ、途中に「;」や「&&」「||」といった、更に別のコマンドを呼び出す事が可能なメタ文字や、リダイレクト用の文字が含まれていない場合にのみ、処理を実行できるようにしています。

このシェルスクリプトを例えば「/usr/local/bin/rsyncshell」というファイル名で保存し、

usermod -s /usr/local/bin/rsyncshell rsync

といった具合に、ログインシェルに設定すれば完成です。

但し、このログインシェルスクリプト、通常のシェルをログインシェルにするよりは制限がかかっている状態になっていますが、

  • 他のプログラムを実行するための回避策が他に無いか?
  • メタ文字に対するチェックが、同期対象のパス名に ASCII 以外の文字が含まれているケースで問題がないか?

といったところは十分に検証されていません。後者の問題が無ければ、通常のシェルを指定するよりはマシ、といった程度に考えて下さい。

また、あくまでも「マスターからダウンロードする rsync」という前提にしています。というのは、不用意にリモート側の $(HOME)/.bashrc などを書き換えないように、と思うと、ダウンロードする方が安全ではないかと思ったからです。

まとめ

全体をまとめると、こんな感じになります。

  • コピー先の root ユーザで rsync を実行し、コピー元からダウンロードする方向にする。
  • rsync の通信は ssh 経由とし、ssh の認証はホストベース認証にする。
  • コピー元となるホストの sshd の設定で、相手のホストと、その時の相手ホスト上のユーザにのみ、ホストベース認証を許可する。
  • コピー元のとなるホストでは、rsync 用のユーザに対して sudo 経由で rsync 実行を許可する。
  • できれば、コピー元の rsync 用ユーザは、制限付きのログインシェルにする。
  • コピー先での rsync の実行は、-e で特定のユーザ名を使った ssh 接続を実現し、--rsync-path で相手側の rsync を sudo 付きで呼び出すようにする。

*1:時間がかかった理由は、本文中にもあるログインシェルの問題と、sudo の requiretty をまるごと無効にしないための方法、あと、我が家の中途半端な IPv6 環境のせい、とか、いろいろありまして...

*2:RHEL / CentOS Ver.6 以降、490 番台に、特定のプログラムが使うアカウントが配置されるケースがあるので、ちょっと気を付ける必要があります。

*3:OpenSSH の ssh-keygen が DSA の鍵を生成する場合、鍵長が 1024 bit に制限される、というのが、RSA が推奨される理由になってます。DSA 自体は 1024 bit より長い鍵長もあるけど、現状、実際に使えるのは 1024 bit のみ、ということのようです。参照: DSA鍵の鍵長 | dodaの日記 | スラド

*4:sshd が動作しているホストであれば、必ずあるはずです。

*5:この時、生成した秘密鍵を置くパスに関して、どこにどんな名前で保存しても、設定ファイルで指定できそうな気もするのですが、ちょっと分かりませんでした。秘密鍵を読み出すのは、ssh クライアントプログラムから呼び出される ssh-keysign というプログラムなのですが、ssh-keysign の manpage を見ても、固定のパス名しか書いてありません。ちょっと謎...

*6:一応、デフォルトは yes です。

*7:ただし、root は除く

*8:「ALL」のところをホスト名で制限したいのですが、ssh 経由の時にこの sudoers のホストによる制限は効かないようです。参照:HowTO: Sudoers Configuration - 「Note: This applies to computers running application services. If you connect to a machine via ssh, this does not apply.」

*9:/etc/sudoers に、「root ALL=(ALL) ALL」といった行があることが前提です。普通、入っていると思います。

*10:もちろん、rbash や rksh などの Restricted Shell も検討しました。

*11:元ネタは http://www.oreillynet.com/linux/blog/2006/05/restricting_rsync_over_ssh.html