リーバスプロキシ/ロードバランサとmod_rpaf

リバースプロキシ/ロードバランサ配下のApache HTTP Server(以降、単にhttpdと記す)ではmod_rpafというモジュールを使用すると、アクセス元のIPアドレスを正しく取得して、そのIPアドレスでログに出力したり、アクセス制御を行ったりすることができるようになります。

今回の記事の前半ではこのmod_rpafについてインストール方法や設定方法について説明します。

後半ではmod_rpafを使ってもアクセス制御ができない問題が発生して、それを解決した経緯などを紹介します。具体的にはロードバランサとしてAmazon Elastic Load Balancingを、プロキシサーバとしてnginxを、バックエンドサーバとしてAmazon Linux 2011.09のhttpdを使ったときにアクセス元IPアドレスによるアクセス制御がうまくできない問題が発生しました。このあたりにご興味のある方は是非ご覧ください。少しだけソースコードを読めるITインフラエンジニアがトラブルシューティングをした簡単な事例としてみていただければと思います。

mod_rpafとは

リバースプロキシ/ロードバランサの配下のApache HTTP Server(以降、単にhttpdと記す)ではそのままではアクセス元のIPアドレスを取得することができず、リバースプロキシ サーバのIPアドレスがアクセス元IPアドレスとしてアクセスログに記録されます。これでは、実際のアクセス元が正しくログに記録されません。また、アクセス元IPアドレスによるアクセス制御も正しく行うことができません。

この対策として、mod_rpafというモジュールを利用します。 アクセス元IPアドレスはリバースプロキシ側でX-Forwarded-Forというヘッダで提供されることが多いでしょう。mod_rpafを使うと、このヘッダのIPアドレスを取得して、httpdの内部のリモートIPアドレスを保持している変数に上書きしてくれます。これにより、正しいアクセス元IPアドレスがアクセスログに記録されます。

mod_rpafの公式サイトは http://stderr.net/apache/rpaf/ です。バージョン0.6で更新が止まっており、保守が継続されていません。

mod_rpafのインストール

まず、ビルドに必要なパッケージをインストールしましょう。ここでは、CentOS 6にインストールする例を紹介します。httpd-develパッケージとgccパッケージおよびそれらに依存するパッケージをインストールします。なお、APR関連のツールもhttpd-develパッケージの依存でインストールされます。

# yum install gcc
# yum install httpd httpd-devel

後半で説明しますが、オリジナルのmod_rpafでは新しめのLinuxディストリビューション(RHEL 6/CentOS 6/Amazon Linux 2011.09)で提供されているhttpdではアクセス制御ができない不具合があります。そのため、次のサイトからtar.gzファイルをダウンロードするか、gitで直接ファイルを取得します。

https://github.com/ttkzw/mod_rpaf-0.6

サイトからダウンロードする方法

https://github.com/ttkzw/mod_rpaf-0.6 にアクセスします。

Downloadをクリックして、Downlaod as tar.gzをクリックすると、ダウンロードできます。後は、展開してインストールします。

$ tar xvzf ttkzw-mod_rpaf-0.6-f96f67c.tar.gz
$ cd ttkzw-mod_rpaf-0.6-f96f67c
$ make
$ sudo make install

gitで取得する方法

$ git clone git://github.com/ttkzw/mod_rpaf-0.6.git
$ cd mod_rpaf-0.6
$ make
$ sudo make install

モジュールファイルを直接ダウンロードする方法

$ mkdir mod_rpaf-0.6
$ cd mod_rpaf-0.6
$ wget https://raw.github.com/ttkzw/mod_rpaf-0.6/master/mod_rpaf-2.0.c
$ sudo /usr/sbin/apxs -i -c -n mod_rpaf-2.0.so mod_rpaf-2.0.c

設定

次の内容の設定ファイル/etc/httpd/conf.d/mod_rpaf.confを作成します。

LoadModule rpaf_module modules/mod_rpaf-2.0.so
RPAFenable On
RPAFsethostname On
RPAFproxy_ips 127.0.0.1 10. 172.16.
RPAFheader X-Forwarded-For

各ディレクティブの説明は以下の通りです。

RPAFenable"On"に設定すると、このモジュールの機能が有効になる。
RPAFsethostname"On"に設定すると、リバースプロキシが付与するヘッダX-Forwarded-HostあるいはX-Hostの値をHostヘッダの値として設定する。
RPAFproxy_ipsリバースプロキシのIPアドレスをスペース区切りで列挙する。オリジナルにはない付加機能として"10."や"172.16."のようなドットで終わるサブネットの記法も使える。なお、この範囲を可能な限り限定してください。リバースプロキシを経由しないアクセス経路があるときには、X-Forwarded-Hostヘッダの偽装の影響を受ける恐れがある。
RPAFheaderリバースプロキシが付与するアクセス元IPアドレスを格納しているヘッダ名を記述する。デフォルトではX-Forwarded-Forが使われる。

設定内容を確認して、再起動すれば利用できます。

$ sudo service httpd configtest
$ sudo service httpd restart

以上で設定は完了です。

ウェブブラウザを使って、リバースプロキシ経由でサーバにアクセスし、アクセスログにアクセス元のIPアドレスが記録されているかを確認してください。

アクセス制御

mod_rpafにより取得したIPアドレスを使って、次のようなアクセス制御を行うことができます。

Order Deny,Allow
Allow from 192.0.2.0/24
Deny from all

アクセス制御に関する不具合の対応

次のような構成でオリジナルのmod_rpaf 0.6では、mod_rpafにより取得したIPアドレスを使って、アクセス制御を行うことができないというトラブルが発生しました。

Amazon Elastic Load Balancing --- nginx --- httpd

ロードバランサとしてAmazon Elastic Load Balancingを、リバースプロキシとしてnginxを、バックエンド ウェブサーバとしてhttpd(Amazon Linux 2011.09のもの、バージョン2.2.21)を使っています。

なお、アクセスログにはアクセス元のIPアドレスが正しく記録されているため、IPアドレスの取得そのものはうまく動いています。

とりあえずの回避策として、リバースプロキシが付与したアクセス元IPアドレスのヘッダX-Forwarded-Forを使ってアクセス制御を行いました。下記の例では、リバースプロキシが付与したヘッダX-Forwarded-Forの値がアクセス許可対象のIPアドレスが一致したときに環境変数allowedを設定し、Allow from env=allowedにより許可するという設定です。しかし、リバースプロキシを経由しないアクセス経路があるときにはX-Forwarded-Forヘッダの偽装のリスクがあります。

SetEnvIf X-Forwarded-For ^192\.0\.2\.[1-9]?[0-9]$ allowed
SetEnvIf X-Forwarded-For ^192\.0\.2\.1[0-9][0-9]$ allowed
SetEnvIf X-Forwarded-For ^192\.0\.2\.2[0-5][0-9]$ allowed

Order Deny,Allow
Allow from env=allowed
Deny from all

この一時的な回避策を実施する一方で、根本的な解決を図るために原因調査を行いました。

RHEL 5/CentOS 5のhttpd 2.2.3では問題なく取得したアクセス元のIPアドレスでアクセス制御できることは確認してます。しかし、RHEL 6/CentOS 5のhttpd 2.2.15では今回問題が起きたAmazon Linux 2011.09のhttpd 2.2.21と同様にアクセス制御ができませんでした。しかし、アクセスログにはアクセス元のIPアドレスが正しく記録されているところから、アクセス元IPアドレスの取得そのものはうまくいっていることがわかります。そのため、mod_rpafとhttpdのソースコードをアクセス制御周りに絞って追いかけてみました。httpd 2.2.22のソースコードで説明します。

httpd 2.2系列でアクセス制御を行っているモジュールはmod_authz_hostです。modules/aaa/mod_authz_host.cを見てみましょう。IPアドレスが一致するかを判断している箇所はfind_allowdeny()関数内のcase T_IPのところです。

static int find_allowdeny(request_rec *r, apr_array_header_t *a, int method)
{
    略
    for (i = 0; i < a->nelts; ++i) {
        略
        case T_IP:
            if (apr_ipsubnet_test(ap[i].x.ip, r->connection->remote_addr)) {
                return 1;
            }
            break;

ここでap[i].x.ipはアクセス制御の設定のAllow/Denyで指定したIPアドレスに関する情報が格納されています。remote_addrにはアクセス元のIPアドレスに関する情報が格納されています。つまり、mod_rpafではこの内容を書き換えているはずです。さらに、apr_ipsubnet_test()という関数では先の2つのIPアドレスの情報からサブネットが一致するかを判断しています。

次に、mod_rpaf-2.0.cを見てみましょう。change_remote_ip()関数内でIPアドレスの書き換えをしています。

r->connection->remote_ip = apr_pstrdup(r->connection->pool, ((char **)arr->elts)[((arr->nelts)-1)]);
r->connection->remote_addr->sa.sin.sin_addr.s_addr = apr_inet_addr(r->connection->remote_ip);

arrにはX-Forwared-Forヘッダから取得したIPアドレスの情報が入っており、そこからremote_ipにアクセス元のIPアドレスが代入されています。その次に、先ほど見かけたremote_addr内のsa.sin.sin_addr.s_addrにIPアドレスを代入しているのがわかります。

再び、httpdのソースコードに戻ります。今度はapr_ipsubnet_test()について確認してみましょう。srclib/apr/network_io/unix/sockaddr.cを見てみます。

APR_DECLARE(int) apr_ipsubnet_test(apr_ipsubnet_t *ipsub, apr_sockaddr_t *sa)
{
#if APR_HAVE_IPV6
    /* XXX This line will segv on Win32 build with APR_HAVE_IPV6,
     * but without the IPV6 drivers installed.
     */
    if (sa->sa.sin.sin_family == AF_INET) {
        if (ipsub->family == AF_INET &&
            ((sa->sa.sin.sin_addr.s_addr & ipsub->mask[0]) == ipsub->sub[0])) {
            return 1;
        }
    }
略
#else
    if ((sa->sa.sin.sin_addr.s_addr & ipsub->mask[0]) == ipsub->sub[0]) {
        return 1;
    }
#endif /* APR_HAVE_IPV6 */

ここでipsubにはアクセス制御の設定のAllow/Denyで指定したIPアドレスに関する情報が、saにはアクセス元のIPアドレスに関する情報が格納されています。

ここで気になるのはAPR_HAVE_IPV6です。APR_HAVE_IPV6が定義されていなければ、特に問題なく判定できそうです。APR_HAVE_IPV6が定義されているときはどうでしょうか。 アドレス ファミリがAF_INET(IPv4)であるかの条件文が先にあります。先のmod_rpafのコースコードにはアドレス ファミリを設定しているところはありませんでした。ここが非常に怪しいですね。そこで、mod_rpafにアドレスファミリを設定するように追加してみました。AF_INET決めうちです。

r->connection->remote_ip = apr_pstrdup(r->connection->pool, ((char **)arr->elts)[((arr->nelts)-1)]);
r->connection->remote_addr->sa.sin.sin_addr.s_addr = apr_inet_addr(r->connection->remote_ip);
r->connection->remote_addr->sa.sin.sin_family = AF_INET;

このように書き替えた後にモジュールをビルドし直すと、アクセス制御ができるようになりました。

ということで、結論としては、問題が発生したhttpdではAPR_HAVE_IPV6を有効にした状態でビルドされておりアドレス ファミリの指定が必要になっていたことと、mod_rpafがアドレス ファミリを指定していなかったことの2つが原因でアクセス制御ができなくなっていたということです。mod_rpafにアドレス ファミリを指定することでこの問題を解決できました。

その他、細かい修正したもの次のURLのgithubに登録しました。

https://github.com/ttkzw/mod_rpaf-0.6

ちなみに、私とは別にmod_rpafをフォークして開発している人がgithub上にいて、そちらのバージョンが0.8になっているので、このgithubに置いたバージョンは0.6のままで名前を付けています。

httpdの各モジュールはそんなに大きいソースコードではないので追いかけようと思えば追いかけられます。ITインフラ エンジニアもたまには普段利用しているミドルウェアのソースコードを読んでみてはいかがでしょうか。

えっと、IPv6対応は機会があれば......

株式会社ハートビーツのインフラエンジニアから、ちょっとした情報をお届けします。