前回の記事では、現在の mod_selinux モジュールの問題点を解決するために複数のタスクキューにワーカースレッドを紐付ける方法を考え、そこから一歩考察を進めて、FastCGIを使ってマルチプロセス実装にすればよりセキュアなWebアプリケーション環境を実現できるのではないか?というアイデアを紹介した。
実際にやってみた。
もちろんSELinuxの権限に紐付けてリクエストの送出先を切り替えるFastCGIモジュールは存在しないので、既存のものに少し手を加える事にする。今回は(たぶん最も広く使われているであろう)mod_fcgidをベースに修正を加える事にする。
mod_fcgidモジュールではFcgidWrapperディレクティブを使って、例えば .php 拡張子を持つURLが指定された場合、FastCGIプロトコルを介して/usr/bin/php-cgiにリクエストを処理させるという指定ができる。
例えばこんな感じ。
AddHandler fcgid-script .php FcgidWrapper /usr/bin/php-cgi .php
で、mod_fcgidのソースを読んでみて気が付いたのだが、この人はFastCGI常駐プログラムが既に起動しているかどうかをチェックするために、内部テーブルをスキャンして、動作中のプロセスを起動したときのコマンドライン文字列(要はFcgidWrapperで指定した内容)と、これから起動するFastCGI常駐プログラムのコマンドライン文字列を比較するという処理を行っている。
そうすると、仮にラッパープログラムのコマンドライン文字列の一部を、HTTPリクエストを処理するセキュリティコンテキスト(あるいはユーザIDでも可)で置換するような仕組みを作ってあげれば、FcgidWrapperを次のように指定するだけで、後はmod_fcgidが善きに計らってくれる。
FcgidWrapper "runcon %(SELINUX_CONTEXT) /usr/bin/php-cgi" .php
例えば、HTTPユーザ alice の場合に %(SELINUX_CONTEXT)が "system_u:system_r:httpd_t:s0:c0" に置換されたら、この設定内容は次のコマンドと等価である。おそらく、認証モジュールで設定するようなHTTP環境変数か何かを使うのが汎用的な実装だろう。兎に角、%(SELINUX_CONTEXT)の部分を動的に置換するというのがポイントである。
FcgidWrapper "runcon system_u:system_r:httpd_t:s0:c0 /usr/bin/php-cgi" .php
もう一点。PHPのような動的コンテンツであれば既存のFastCGIの設定と同様にできるが、静的コンテンツの場合、apache/httpdはhandlerフックの一番最後で他のモジュールが処理できないタイプのHTTPリクエストを処理する…という流れになる。
つまり、ap_hook_handler() に APR_HOOK_LAST を付けて関数ポインタを登録し、静的コンテンツを読むだけのFastCGI常駐プログラムを実行するような拡張が必要になる。
この2点の魔改造を加えた mod_fcgid がこちら。
https://github.com/kaigai/mod_fcgid
KaiGai版 mod_fcgid のインストールと設定
mod_fcgid(魔改造版)のインストールは以下の通り。
$ git clone git://github.com/kaigai/mod_fcgid.git $ cd mod_fcgid $ git checkout --track origin/defhnd $ ./configure.apxs && make $ sudo make install
Apache/httpdの設定ファイルを編集。ここでは、.php ファイルに対してrunconを介して/usr/bin/php-cgiを実行するのと同時に、静的コンテンツに対してはfcgi-cat(後述)コマンドを実行する。
runconの引数に%(AUTHENTICATE_SELINUX_TYPE)と%(AUTHENTICATE_SELINUX_RANGE)が付いているが、これは実行時にHTTP環境変数と置換される。で、この環境変数を設定するのがauthn_dbd_moduleで、AuthDBDUserPWQueryが2個以上のカラムを返すとき、それらの内容はHTTP環境変数として後続のモジュールで参照する事ができる。
LoadModule fcgid_module modules/mod_fcgid.so LoadModule dbd_module modules/mod_dbd.so LoadModule authn_dbd_module modules/mod_authn_dbd.so DBDriver pgsql DBDParams "host=localhost dbname=web user=apache" AddHandler fcgid-script .php FcgidWrapper "/bin/runcon -t %(AUTHENTICATE_SELINUX_TYPE) \ -l %(AUTHENTICATE_SELINUX_RANGE) \ /usr/bin/php-cgi" .php FcgidDocumentWrapper "/bin/runcon -t %(AUTHENTICATE_SELINUX_TYPE) \ -l %(AUTHENTICATE_SELINUX_RANGE) \ /usr/local/bin/fcgi-cat" <Directory "/var/www/html"> # BASIC authentication AuthType Basic AuthName "Secret Zone" AuthBasicProvider dbd AuthDBDUserPWQuery "SELECT upasswd, selinux_type, selinux_range \ FROM uaccount WHERE uname = %s" Require valid-user </Directory>
次に、認証DBのセットアップ。uaccountテーブルにBASIC認証の情報をインプットしておく。$PGDATAはPostgreSQLのDBクラスタのトップディレクトリ。まぁ、COPYコマンドで読めればドコでもいいんですが。
$ htpasswd -c $PGDATA/uaccount.master alice New password: Re-type new password: Adding password for user alice $ htpasswd $PGDATA/uaccount.master bob New password: Re-type new password: Adding password for user bob $ createdb web
$ pgsql web web=# CREATE USER apache; CREATE ROLE web=# CREATE TABLE uaccount ( uname text primary key, upasswd text, selinux_type text, selinux_range text ); CREATE TABLE web=# GRANT SELECT on uaccount TO public; GRANT web=# COPY uaccount(uname,upasswd) FROM '/opt/pgsql/uaccount.master'; web=# COPY uaccount(uname,upasswd) FROM '/opt/pgsql/uaccount.master' DELIMITER ':'; COPY 2 web=# UPDATE uaccount SET selinux_type = 'httpd_t'; UPDATE 2 web=# UPDATE uaccount SET selinux_range = 's0:c0' WHERE uname = 'alice'; UPDATE 1 web=# UPDATE uaccount SET selinux_range = 's0:c1' WHERE uname = 'bob'; UPDATE 1 web=# SELECT * FROM uaccount; uname | upasswd | selinux_type | selinux_range -------+---------------------------------------+--------------+--------------- alice | $apr1$HQtZm8Sz$ZwXm9tx6Kz7g2wiE1LBUZ/ | httpd_t | s0:c0 bob | $apr1$YJWn7hx3$iJyazYClFeFF0n8EgIMSi. | httpd_t | s0:c1 (2 rows)
次に、静的ドキュメントを参照するFactCGI常駐プログラムのfcgi-catをビルドして /usr/local/bin にインストールする。こやつのビルドには fcgi-devel パッケージも必要です。
$ git clone git://github.com/kaigai/toybox.git $ cd toybox $ gcc -g fcgi-cat.c -lfcgi -o fcgi-cat $ sudo install -m 755 fcgi-cat /usr/local/bin
最後に、SELinuxのセキュリティポリシーを追加し、Booleanを調整して設定は終わり。
$ make -f /usr/share/selinux/devel/Makefile webapps.pp $ sudo semodule -i webapps.pp $ sudo setsebool httpd_can_network_connect_db on
動かしてみた
お試しとして、以下の内容のPHPスクリプトを実行させてみる。
<?php echo "<h3>".selinux_getcon()."<h3>\n"; echo "<h3>username = ".$_SERVER["REMOTE_USER"]."</h3>\n"; phpinfo(); ?>
ユーザ alice としてアクセスした場合。確かにDBで指定した通り、セキュリティコンテキストの末尾が ":s0:c0" となっている。
一方、ユーザ bob としてアクセスした場合。今度はセキュリティコンテキストの末尾が ":s0:c1" となっている。Server APIの欄が "CGI/FastCGI" となっている点にも注目。
では、静的ドキュメントを参照した場合。2つの画像ファイル test00.jpg と test01.jpg のセキュリティコンテキストはそれぞれ以下のように設定されているため、alice は test00.jpg を参照できるが、test01.jpg は見えないはず。
$ ls -Z /var/www/html -rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 mytest.php -rwxr--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0:c0 test00.jpg -rwxr--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0:c1 test01.jpg
これが test00.jpg を参照したケース。正しく読み込めている。
今度は test01.jpg を参照した場合。サーバは 403 Forbidden を返している。ヒャッハー!
画像は省略するが、これをユーザbobで試した場合は逆の結果となる。
もちろん、これらはFastCGIで動作しているので、psコマンドを叩くと常駐プロセスの様子が見える。
確かにユーザ alice と bob それぞれのHTTPリクエストを処理するために、別個のプロセスが起動している。
[root@iwashi ~]# ps -AZ | grep php-cgi system_u:system_r:httpd_t:s0:c0 23960 ? 00:00:00 php-cgi system_u:system_r:httpd_t:s0:c1 24062 ? 00:00:00 php-cgi [root@iwashi ~]# ps -AZ | grep fcgi-cat system_u:system_r:httpd_t:s0:c0 24098 ? 00:00:00 fcgi-cat system_u:system_r:httpd_t:s0:c1 24107 ? 00:00:00 fcgi-cat
まとめ
今までの mod_selinux と違い、この方法では全てのDSOモジュールに作用させるという事はできない。
PHPならPHP用の、RubyならRuby用の設定がそれぞれ必要となる。これは確かにデメリット。
その一方で、HTTPユーザ毎にアプリを実行するメモリ空間を分けられるという事は、セキュリティの観点からは非常に大きなアドバンテージである。Web管理者がその気になれば、Apache/httpdでは一切Webアプリの実行を行わず、ある種のアプリケーションサーバとして振る舞うFastCGI常駐プログラムが、HTTPユーザ毎に閉じたコンテナ環境を提供するという事も可能だろう。
セキュリティの強化を目的とするモジュールなので、ここのアドバンテージはでかい、と思う。