Raccoon Tech Blog [株式会社ラクーン 技術戦略部ブログ]

株式会社ラクーン 技術戦略部より、tipsやノウハウなど技術的な話題を発信いたします。

ネットワーク

iptablesを利用してNATサーバを構築

こんにちは、えのもとです。

弊社では本番リリース直前検証環境として、ステージング環境を利用しています。
この環境が古くなってしまったため、環境再構築を実施することになりました。

再構築する上で、今後のこともあるので再構築しやすい環境に切り替えることにしました。
構築上でポイントとなったのは以下の2つです。
  • 本番環境はVMで稼働しているので、そのバックアップを活用する
  • サーバ設定(ネットワーク設定など)はできる限り変更したくない
これらを達成するため、閉鎖されたネットワークを構築しその中に環境を構築することにしました。そこで問題となるのは閉鎖された環境内外との通信なのですが、iptablesを利用してnatサーバを構築することで解決しました。今回はこのiptablesの検証の様子を紹介します。

NATとiptablesについて

今回環境を構築するに当たり、以下を参考にしました。

@IT:第10回 IPパケットの構造とIPフラグメンテーション
@IT:natテーブルを利用したLinuxルータの作成

要点だけ羅列すると以下のような内容になります。
  • パケット内IPヘッダに送信元アドレスと宛先アドレスが記載されている。
  • 送信元アドレスを変換するNATのことをSNAT
    宛先アドレスを書き換えるNATのことをDNATと呼ぶ。
  • iptablesのSNATはPOSTROUTINGチェインで、DNATはPREROUTINGチェインで行う。


検証内容と環境の紹介


存在しないIPアドレス宛てのパケットをiptablesでnatし、別のIPアドレスで受けとることができるか検証しました。検証には以下のような環境を利用しました。

セグメント
  1. A(192.168.100.0/24)
  2. B(192.168.200.0/24)
仮想マシン
  1. vm01           eth0:【 192.168.100.1 】
  2. vm02           eth0:【 192.168.100.2 】 eth1:【 192.168.200.2 】
  3. vm03           eth0:【 192.168.200.3 】
  4. gateway_A   eth0:【 192.168.100.254 】
  5. gateway_B   eth0:【 192.168.200.254 】

eno_2015120901

vm01から存在しないIP【 192.168.100.3 】 宛にパケットを送り、
vm02でNATさせてvm03のIP【 192.168.200.3 】へ届けます。
vm01からするとIP【 192.168.100.3 】 が存在するように見えます。(赤線)
実際はvm02を経由してセグメントBにいるvm03へ届きます。(青線)

eno_2015120902


事前確認

iptablesに余計な設定があると誤作動の原因になるので、一旦どのサーバもクリアしておきます。

# iptables クリア
iptables -F
iptables -F -t nat
# iptables 確認
iptables -L
iptables -L -t nat
# iptables 保存
service iptables save

また、vm02はパケットを転送する必要があるので以下の設定を加えます。
vi /etc/sysctl.conf
#net.ipv4.ip_forward=0
net.ipv4.ip_forward=1

sysctl -p



ステップ 1 : vm01にルーティング追加

手始めにvm01から【 192.168.100.3 】宛のpingを実行し、その様子をvm01とvm02の両方でtcpdumpを使いパケットをキャプチャして確認します。

# vm01
[root@vm01 ~]# ping 192.168.100.3
PING 192.168.100.3 (192.168.100.3) 56(84) bytes of data.
From 192.168.100.1 icmp_seq=2 Destination Host Unreachable
From 192.168.100.1 icmp_seq=3 Destination Host Unreachable
From 192.168.100.1 icmp_seq=4 Destination Host Unreachable

# vm02
[root@vm02 ~]# tcpdump -i any src host 192.168.100.1 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
13:28:24.003479 ARP, Request who-has 192.168.100.3 tell 192.168.100.1, length 46
13:28:25.003469 ARP, Request who-has 192.168.100.3 tell 192.168.100.1, length 46
13:28:26.003479 ARP, Request who-has 192.168.100.3 tell 192.168.100.1, length 46
13:28:28.004484 ARP, Request who-has 192.168.100.3 tell 192.168.100.1, length 46

vm01から【 192.168.100.3 】へパケットを送る場合、同一セグメント内なのでゲートウェイなどは経由せず直接通信することになります。直接通信するためには相手のMACアドレスを知る必要があります。ARPリクエストをブロードキャストへ送信しMACアドレスを探している様子が確認できました。

これを無理やりvm02へ送るようにvm01にスタティックルーティングを追加します。

# vm01
[root@vm01 ~]# ip route add 192.168.100.3 via 192.168.100.2 dev eth0
[root@vm01 ~]# ip route show
192.168.100.3 via 192.168.100.2 dev eth0
192.168.100.0/24 dev eth0  proto kernel  scope link  src 192.168.100.1
169.254.0.0/16 dev eth0  scope link  metric 1002
default via 192.168.100.254 dev eth0

再度vm01から【 192.168.100.3 】へpingを実行し確認します。

# vm01
[root@vm01 ~]# ping 192.168.100.3
PING 192.168.100.3 (192.168.100.3) 56(84) bytes of data.
From 192.168.100.2: icmp_seq=2 Redirect Host(New nexthop: 192.168.100.3)
From 192.168.100.2: icmp_seq=3 Redirect Host(New nexthop: 192.168.100.3)
From 192.168.100.2: icmp_seq=4 Redirect Host(New nexthop: 192.168.100.3)
From 192.168.100.2 icmp_seq=2 Destination Host Unreachable
From 192.168.100.2 icmp_seq=3 Destination Host Unreachable
From 192.168.100.2 icmp_seq=4 Destination Host Unreachable

# vm02
[root@vm02 ~]# tcpdump -i any src host 192.168.100.1 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
13:48:19.136230 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 28934, seq 1, length 64
13:48:20.135528 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 28934, seq 2, length 64
13:48:21.135501 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 28934, seq 3, length 64
13:48:22.135497 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 28934, seq 4, length 64

【 192.168.100.3 】宛のパケットがvm02へ届くようになりました。



ステップ2 : vm02にDNAT設定を追加

次に存在しない宛先IPアドレス【 192.168.100.3 】をvm03【 192.168.200.3 】変換するため、vm02のiptablesにDNATの設定を追加します。

# vm02
[root@vm02 ~]# iptables -t nat -A PREROUTING -d 192.168.100.3 -i eth0 -j DNAT --to-destination 192.168.200.3
[root@vm02 ~]# iptables -L -n -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DNAT       all  --  0.0.0.0/0            192.168.100.3       to:192.168.200.3

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination


vm01から【 192.168.100.3 】へpingを実行し、vm02とvm03で確認します。

# vm01
[root@vm01 ~]# ping 192.168.100.3
PING 192.168.100.3 (192.168.100.3) 56(84) bytes of data.

# vm02
[root@vm02 ~]# tcpdump -i any src host 192.168.100.1 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
13:55:28.318556 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 30982, seq 1, length 64
13:55:28.318586 IP 192.168.100.1 > 192.168.200.3: ICMP echo request, id 30982, seq 1, length 64
13:55:29.318498 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 30982, seq 2, length 64
13:55:29.318513 IP 192.168.100.1 > 192.168.200.3: ICMP echo request, id 30982, seq 2, length 64
13:55:30.318483 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 30982, seq 3, length 64
13:55:30.318496 IP 192.168.100.1 > 192.168.200.3: ICMP echo request, id 30982, seq 3, length 64
13:55:31.318515 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 30982, seq 4, length 64
13:55:31.318529 IP 192.168.100.1 > 192.168.200.3: ICMP echo request, id 30982, seq 4, length 64

# vm03
[root@vm03 ~]# tcpdump -i any src host 192.168.100.1 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
13:55:28.381794 IP 192.168.100.1 > 192.168.200.3: ICMP echo request, id 30982, seq 1, length 64
13:55:29.381711 IP 192.168.100.1 > 192.168.200.3: ICMP echo request, id 30982, seq 2, length 64
13:55:30.381702 IP 192.168.100.1 > 192.168.200.3: ICMP echo request, id 30982, seq 3, length 64
13:55:31.381730 IP 192.168.100.1 > 192.168.200.3: ICMP echo request, id 30982, seq 4, length 64

vm03にvm01のPINGが届いていることが確認できました。



ステップ3 : vm02にSNAT(NAPT)設定を追加

vm01から送るpingは無事vm03へ届けることができたのですが、vm03がセグメントAの情報を持っていないため、送信元であるvm01に返信することができません。そのため、vm02へSNAT(NAPT)を追加します。

# vm02
[root@vm02 ~]# iptables -t nat -A POSTROUTING -o eth1 -s 192.168.100.0/24  -j MASQUERADE
[root@vm02 ~]# iptables -L -n -t nat
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DNAT       all  --  0.0.0.0/0            192.168.100.3       to:192.168.200.3

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  192.168.100.0/24     0.0.0.0/0

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

確認します。

# vm01
[root@vm01 ~]# ping 192.168.100.3
PING 192.168.100.3 (192.168.100.3) 56(84) bytes of data.
64 bytes from 192.168.100.3: icmp_seq=1 ttl=63 time=0.235 ms
64 bytes from 192.168.100.3: icmp_seq=2 ttl=63 time=0.164 ms
64 bytes from 192.168.100.3: icmp_seq=3 ttl=63 time=0.164 ms

# vm02
[root@vm02 ~]# tcpdump -i eth0 src host 192.168.100.1 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
14:04:40.915680 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 35078, seq 1, length 64
14:04:41.915507 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 35078, seq 2, length 64
14:04:42.915500 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 35078, seq 3, length 64
14:04:43.915525 IP 192.168.100.1 > 192.168.100.3: ICMP echo request, id 35078, seq 4, length 64

# vm02
[root@vm02 ~]# tcpdump -i eth1 src host 192.168.200.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 65535 bytes
14:04:40.915714 IP 192.168.200.2 > 192.168.200.3: ICMP echo request, id 35078, seq 1, length 64
14:04:41.915522 IP 192.168.200.2 > 192.168.200.3: ICMP echo request, id 35078, seq 2, length 64
14:04:42.915513 IP 192.168.200.2 > 192.168.200.3: ICMP echo request, id 35078, seq 3, length 64
14:04:43.915546 IP 192.168.200.2 > 192.168.200.3: ICMP echo request, id 35078, seq 4, length 64

# vm03
[root@vm03 ~]# tcpdump -i any src host 192.168.200.2 -n
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
14:04:40.978924 IP 192.168.200.2 > 192.168.200.3: ICMP echo request, id 35078, seq 1, length 64
14:04:41.978721 IP 192.168.200.2 > 192.168.200.3: ICMP echo request, id 35078, seq 2, length 64
14:04:42.978718 IP 192.168.200.2 > 192.168.200.3: ICMP echo request, id 35078, seq 3, length 64
14:04:43.978754 IP 192.168.200.2 > 192.168.200.3: ICMP echo request, id 35078, seq 4, length 64

無事にpingが返ってくることが確認できました。



まとめ

作成した環境を利用して、どんなサービスが利用できるのかも検証しました。

動作OK : ssh http https dns mysql
動作NG : ftp oracle

ipヘッダ以外でipのやり取りをするものについては正常に動作しないのですが、動作したものについては、特に問題もなく使えることが分かりました。

以上です。何かの役に立てばと思います。

DoS攻撃からサーバーを守る、mod_dosdetector導入事例のご紹介

こんにちは、羽山です。今回は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氏に感謝いたします。

記事検索