読者です 読者をやめる 読者になる 読者になる

mod_selinuxのアーキテクチャを考える(後編)

OSS/Linux

前回の記事では、現在の 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ユーザ毎に閉じたコンテナ環境を提供するという事も可能だろう。
セキュリティの強化を目的とするモジュールなので、ここのアドバンテージはでかい、と思う。