こんにちは、羽山です。今回はDoS攻撃の話題です。

弊社の運営するファッション・雑貨向けB2Bサイトスーパーデリバリー(以下 SUPER DELIVERY)にはブラウザからの普通のアクセスを逸脱した過度なHTTPリクエストがしばしばやってきます。大半は独自クローラーなどの悪意がある攻撃とは言えないものですがサイトに与える負荷は高く対応の必要がありました。
 
今回の記事では弊社で行ったDoS攻撃対策の顛末をお送りします。
記事の性質上、設定内容など一部ぼやかして表現している点もありますがご容赦ください。
また、当記事では過度なHTTPリクエストをすべてDoS攻撃という呼称で統一しています。


DoS攻撃とは?
そもそもDoS攻撃と一口に言ってもTCPレベルからHTTPレベルまで様々な手法が存在します。
例えばTCPレベルで有名なのはSYN floodという攻撃で、接続開始を表すSYNパケットを攻撃対象に大量に送りつけることでサーバーのリソースを大量に消費させます。しかし幸いなことにTCPレベルのDoS攻撃の多くはFirewallやLoadBalancer(以下LB)に保護機能があることが多く、特別な対策を意識するケースは減ってきています。
しかしその一方で問題が顕在化しているのはHTTPレベルのDoS攻撃です。例えば「正常なHTTPアクセスを大量に行う」などステートレスな手法では防げないものが多いため、防御する仕組みや機器は複雑・高度・高価になりがちです。 


サーバー構成の説明
まずは対策を行う弊社のサーバ環境を簡単に説明します。
以下の図のようにFirawallとL7のLB以下に複数台のウェブサーバが配置されて、LBから各ウェブサーバはリバースプロキシの構成をとっています。
ウェブサーバは主にApache2.2/2.4系を利用しています。

l7proxy

対策方法の検討
1. 専用アプライアンスの導入
ある程度の予算がとれるならばDoS対策専用のアプライアンス導入が第一候補として考えられます。
すでに多岐にわたるアプライアンスが出そろっていてある程度成熟しているといえるでしょう。
シグネチャや振る舞いに基づいた保護を基礎として、SQLインジェクション[1]などのWEBアプリケーションの脆弱性まで保護してくれるものもあります。

[1] SQLインジェクションなどはアプリ側できちんと対策する前提ではありますが、アプライアンス導入には安全性を保証する客観的な指標となるメリットがあります。DoS対策アプライアンスは元々L7をベースに動作しているためHTTPプロトコルを詳細に解析するたぐいの機能追加とは相性がよいことから、付加価値を高めるために両者が同時に提供されることがあります。


弊社の場合、「まずはDoS対策を導入」というステージだったため、いきなり大きく予算を取るリスクを避けたかったこともありアプライアンスの導入は保留にして別の案を検討しました。

2. 各ノードで個別にDoS検査
次に検討したのは各ノード、つまりウェブサーバのApacheレベルでの検査です。各ノードでの検査の場合はソフトウェアで対応できるので必要な費用を最低限におさえることができます。
DoS攻撃によるアクセスがLBによって各サーバに分散されてしまうため検出しにくいという弱点がありますが、幸いSUPER DELIVERYのサーバはLBがノードを選択する際に持続接続機能を利用していて同一クライアントは同一ノードに転送されるため、ノード側でのDoS検査でも上流での検査とさほど変わらないレベルで実施可能でした。
Apache側での防御手段を調べたところmod_dosdetectorというid:stanaka氏が開発・公開してくださっているモジュールがApache2.2/2.4で利用可能で弊社の要件にも近いということが分かりました。
LB配下では接続元IPがLBの内部IPに置き換えられてしまい利用できないためX-Forwarded-Forヘッダを代わりに確認する必要がありますが、dosdetectorはその機能にも対応しています。

まずはこの案を採用してみることになりました。


mod_dosdetectorの基本機能
すでにいくつものサイトで紹介されているので詳細は省略しますが、基本的には下記のような動作をします。
1. クライアントのIPアドレス一覧を共有メモリに保持
2. IPに紐付けてアクセス数やアクセス時間を記録
3. 一定時間内のアクセス数が設定した閾値を超えたらSuspectDoSという環境変数に1をセットする

mod_dosdetector自体の動作は以上で終わりです。
環境変数に入れるだけの動作なのでその後はmod_rewriteなど既存のモジュールで柔軟に処理できます。


いくつか課題が発生
基本機能はほぼ要件にマッチしたのですが、いくつか解決が必要な課題が出てきました。
以下に挙げる点はrewriteで頑張れば回避できるものもありますが、複雑なrewriteルールを避けたかったため、結論としてはmod_dosdetector自体を改修して機能追加することで解決しました。
弊社で利用しているモジュールを記事の最下部でダウンロードできるようにしています。
LB配下の一般的な環境で使いやすいよう最適化しています、無保証ですが興味ある方はご利用ください。

課題1 X-Forwarded-ForからのIP抽出方法
mod_dosdetectorは接続元IPの代わりにX-Forwarded-Forを参照するDoSForwardedというオプションがあります。
基本的にはこの設定で問題ないのですが、X-Forwarded-Forに複数のIPが含まれるパターンで問題となる場合があります。

mod_dosdetectorのX-Forwarded-ForからIPの選択方法は下記のようになっています。
1. X-Forwarded-Forの先頭からカンマ(含まない場合は末尾)までの文字列をIPとして利用
2. X-Forwarded-Forヘッダが存在していて、その文字列が有効なIPではなかった場合はDoS検査の対象外になる

ここで2点問題が出てきますが、その話の前にX-Forwarded-Forヘッダの一般的な動きを理解しましょう。
一般的にプロキシサーバやLBを経由するとX-Forwarded-Forの末尾に接続元IPを追加して、接続元IPを自身のIPとした新しいHTTPリクエストを目的のサーバに送信します。

X-Forwarded-Forヘッダーについて

通常は社内プロキシで転送する場合はX-Forwarded-Forに内部IPを残さない設定にしますが、設定が不十分でそのまま残ってしまってるケースもよくあります。
この例では最終的なX-Forwarded-Forの値は192.168.0.100, 203.0.113.54, 198.51.100.23となるため、検査対象のIPアドレスを先頭から取得すると正しい検査を行えません。

dosdetectorの標準動作である先頭のIPを採用した場合、下記の2つの問題が発生します。

1. 社内プロキシサーバ経由のアクセスの場合に内部ネットワークのIPアドレスが入っているケースがある
2. X-Forwarded-Forヘッダは容易に偽造可能

1つめは不適切なIPアドレスを元にDoS検査を行ってしまうことになり、全く関係のないクライアント同士が同一IPという扱いを受けてしまったり、内部IPを無視する設定を行った場合は検査が作動しなかったりします。
2つめは根本的な問題で、原則としてSUPER DELIVERYネットワークの管理下にあるLBが付与した一番最後のIP以外はすべて信頼できません。それ以外のIPは偽造可能なため例えばX-Forwarded-Forヘッダに毎回異なるIPを適当に設定してアクセスすれば実質DoS検査を無効化できてしまいます。

課題2 X-Forwarded-Forが存在しない場合
X-Forwarded-Forヘッダが存在しなかった場合、dosdetectorは接続元IPを利用してDoS検査を行います。
しかし一般的にLBからのリバースプロキシ構成ではX-Forwarded-Forヘッダを常に付与する設定にできるので、LBを経由するアクセスなのにX-Forwarded-Forヘッダが付与されないということはありません。

しかし各ノードに流れてくる一部のアクセスにはX-Forwarded-Forヘッダが付与されないことがあります。それらはLBから生存確認のために送るheartbeatであったり、サーバー間通信などシステム内部的なアクセスです。X-Forwarded-Forヘッダがない場合に接続元IPを代わりに利用してしまうと、これらの通信を阻害してしまい問題になることがあります。

課題3 DoS検査の対象について
今回DoS検査の対象としたサーバではいくつかのシステムが並列で稼働していて、そのうち一部をDoS検査から除外する必要がありました。それらはシステムが内部的にAjaxで呼び出すAPIなど、連続で叩かれることが元々想定されているものなどです。
根本的にはアクセス頻度の大きく異なるサービスが同一のサーバに同居していることが問題なんですが、レガシーなシステムも含むためあまり手を入れずに対処したいという事情もありました。
dosdetectorは検査対象がVirtualHost単位になるため、ディレクトリ毎に対象・除外の設定を行うことはできません。
mod_rewriteで除外したいパスを表面上素通しすることは可能ですがDoS検査自体は動作するため、対象外のページでもアクセスがカウントされ、そのまま検査対象のページに移動したらDoS検査に引っかかるなんてことがおきてしまいます。

課題4 社内IP
当然ですが弊社社内の人間は頻繁にSUPER DELIVERYにアクセスします。
そして社内ネットワークのグローバルIPは1つなので簡単にDoS検査に引っかかってしまうことが分かりました。mod_rewriteで省くことは簡単なのですが、mod_rewriteの設定がごちゃごちゃするのでどうせならこれも要件に入れちゃえ!と、ついでに入れました。


モジュールの変更ポイント
<DoSIgnoreIpRange>
※複数行で設定可能
DoS検査の対象から除外するIP範囲を指定可能で、下記3種類の指定方法を認識します。
・192.168.0.0/16
・10.0.0.0/255.0.0.0
・203.0.113.56

<IncludePath/ExcludePath>
※複数行で設定可能
DoS検査を行う対象をrequest_uriで絞り込みます。
1. ExcludePath優先、ExcludePathにマッチしたらDoS検査を行わない
2. IncludePathを次に検査、IncludePathが1つも設定されていない場合はIncludePathに / が指定されているのと同じ動作をする
3. IncludePathが1つ以上ある場合は明示的にIncludePathに指定されたパスのみDoS検査の対象とする

IncludePath/ExcludePathともに、複数設定した場合はOR結合です。

X-Forwarded-Forに複数IPを含む場合の処理を変更
X-Forwarded-Forヘッダの末尾からIP候補を探します。カンマ区切りで順に取得して、IPとして無効な文字列やDoSIgnoreIpRangeに含まれるIPを除外した最初(より末尾に近い)のIPをDoS検査対象とします。
これで X-Forwarded-Forヘッダを偽装されても正しくDoS検査を行うことができます。

DoSForwardedがOnでX-Forwarded-Forが存在しない場合の処理を変更
DoSForwardedがONの場合は、X-Forwarded-Forが存在しなければDoS検査を行わないよう変更しました。


導入
ダウンロードして適当なディレクトリに解凍

同じディレクトリに上記ファイルをダウンロード

apxsのパスが異なる場合はMakefileを修正します。
以下、パッケージ版のapacheを利用している前提で進めます。

$ patch < mod_dosdetector.patch
$ make
$ make install
$ vi /etc/conf/httpd.conf
モジュールが読み込まれていることを確認します。 
LoadModule dosdetector_module modules/mod_dosdetector.so
具体的な数値や設定を載せるといろいろと怒られるのであくまで設定例としてご紹介します。
下記は弊社で実際に運用している設定とは異なります。
 
DoSDetection On
DoS検査機能をOn
 
DoSForwarded On
接続元IPの代わりにX-Forwarded-Forを利用する場合はOn
 
DoSPeriod 60
DoSThreshold 200
DoSHardThreshold 300
DoSBanPeriod 30
DoSTableSize 100
DoSPeriodはDoS検出する時間単位で、指定した秒数の間にDoSThreshold回以上のアクセスがあるとSuspectDoS=1がセットされます。
さらに、DoSHardThreshold回以上のアクセスがあるとSuspectHardDoS=1もセットされます。
DoS判定されるとDoSBanPeriodで指定した秒数の間SuspectDoS=1がセットされ続け、その後いったん規制がクリアされます。
DoSTableSizeは保持するIP一覧の数で、この数だけ共有メモリに領域が確保されます。そのため多くすれば検出の幅は広がりますが、IP一覧からIPを検索する負荷が上昇するため効率が下がります。IP一覧はLRUで管理されるため頻繁にアクセスしてくるDoS攻撃のIPは残りやすため、よほど高負荷な環境でなければ100もあれば十分だと考えられます。

DoSIgnoreContentType ^(image/|application/|text/javascript|text/css)
Apacheがローカルで解決できるレベルでコンテンツタイプによる除外を行います。
しかし、mod_proxyなど外部リソースから動的にコンテンツタイプが返却される場合は除外できないため、この設定に頼ってしまうと危険です。動的コンテンツの場合は後述のmod_rewriteで除外することを検討します。

DoSIgnoreIpRange 192.168.0.0/16
DoSIgnoreIpRange 172.16.0.0/12
DoSIgnoreIpRange 10.0.0.0/8
DoSIgnoreIpRange 203.0.113.56
内部IPと特定のIP(社内ネットワークのグローバルIPを想定)を除外する例です。サーバー間でなんらか直接やりとりしている場合やLBからのheartbeatなどのために内部IPは除外しておいた方が無難です。必要に応じて社内ネットワークのグローバルIPなども除外します。

DoSIncludePath /path1/
DoSIncludePath /path2/
DoSExcludePath /path1/exclude1
DoSExcludePath /path1/exclude2
DoSIncludePathにはDoS検査対象のパスを前方一致で指定、未指定の場合はすべてのパスが対象となります。
DoSExcludePathにはDoS検査対象から除外するパスを前方一致で指定します。
DoSExcludePathがDoSIncludePathよりも優先され、両方とも複数のパスを設定した場合はOR結合となります。

RewriteCond %{ENV:SuspectDoS} =1
RewriteCond %{HTTP_USER_AGENT} !googlebot [NC]
RewriteRule .  - [E=DoS:1]
dosdetectorの結果を受け取ってmod_rewriteで除外する例です。
ここではSuspectDoS変数に1がセットされていて、かつユーザーエージェントにgooglebotを含まない場合を最終的なDoS対象としていて、新しい変数DoSに1をセットします。

ErrorDocument 500 /error503.html
RewriteCond %{ENV:DoS} =1
RewriteCond %{REQUEST_URI} !=/error503.html
RewriteRule . - [R=503,L]
CustomLog /var/log/httpd/sd-static-dosdetector.log combinedr env=DoS
最終的なアクションです。DoS=1がセットされている場合は503ページを表示しつつログも出力します。
503ページは /error503.html というパスに存在する前提です。


最後に
本モジュールを導入して2ヶ月以上経ちますが問題なく安定稼働しています。
大規模サイトでは当たり前のように行われているであろうDoS攻撃への対策ですが、中・小規模のサイトではなかなかノウハウを含め情報が行き渡っていないのではないでしょうか?
かく言うSUPER DELIVERYでもようやく必要性が出てきたところでした。この記事がそういったステージの方の一助になることを願います。
 
最後になりましたが、 非常に有用なモジュールを開発および公開してくださっているid:stanaka氏に感謝いたします。