OXY NOTES

fluentdとNorikraでDoS攻撃を遮断し、メールで通知する方法

ビックデータ時代の柔軟なサーバ管理方法

前回の投稿NorikraQueryについて学んだので、今回は実用的な例でQueryを作成してみます。

Nginxのログを取得し、DoS攻撃らしき兆候を察知したらメールで通知。

さらにDoS攻撃と認定したら、即アクセスを遮断するという仕組みを実装します。


目次


fluentdとNorikraの連携について

ログファイルの取り込みにはfluentdを利用し、そこからNorikraへ集計するデータを渡し、条件に一致したらイベントを再びflentdに戻します。

受け渡しにはfluent-plugin-norikraというプラグインを利用します。
flentdからNorikraにはNorikraOutput、反対にNorikraからfluentdへはNorikraInputを利用します。

fluent-plugin-norikraのインストール

ではfluent-plugin-norikraをインストールします。
解説にある通りgemで簡単にインストールできます。

# td-agent-gem install fluent-plugin-norikra
(省略)
6 gems installed

以下の警告が出る場合がありますが、td-agentの次期バージョンで修正されるとのこと。

WARN: Unresolved specs during Gem::Specification.reset:
      json (>= 1.4.3)
WARN: Clearing out unresolved specs.

NorikraOutputでfluentからNorikraへイベントを渡す

今回はNginxのaccess.logを利用します。

NginxのログはLTSV形式でfluentdに渡しているという前提です。
その辺の解説は「以前の投稿」でまとめたので必要な方はご覧ください。

<source>
    type tail
    path /var/log/nginx/access.log
    pos_file /var/log/td-agent/fluent.log.pos
    tag nginx.access
    format ltsv
    time_key time
    time_format %d/%b/%Y:%H:%M:%S %z
</source>

<match nginx.access>
    type copy
    (省略)
    <store>
        type norikra
        norikra localhost:26571
        target_string nginx
        # remove_tag_prefix nginx.access.status_count
        # target_map_tag ture
    </store>
</match>

以前の解説で紹介した設定の最後にNorikraOutput用の設定を追加しただけです。

typeは「norikra」でfluent-plugin-norikraを指定。

norikraは自分のサーバ内で処理するので「localhost:26571」。

target_stringNorikraのTarget名を指定。

必要な設定は上記の3つだけです。

remove_tag_prefixで他のプラグインと同じように不要なtagを削除。

target_map_tagを有効にするとfluentdのTagがそのままNorikraTargetに変換される。上記のremove_tag_prefixと組み合わせて使用することが多いと思います。
個人的にはtarget_stringで明示的に指定したほうが直感的でオススメです。

以上の設定でNginxのaccess.logがNorikraにイベントとして渡されます。


nginxのaccess.logでDoS攻撃を察知する

ハッカー集団Anonymousによって再び世間を賑わせているDoS攻撃。
不正な通信の場合は問答無用で遮断するだけなので対応は簡単です。

しかしhttpによるアクセスの場合は全ての通信を遮断することはできません。
そこで「アクセス数の多いIPをリアルタイムに抽出して遮断する」といった対策が必要になります。

そのような要求に対して、当サイトではiptablesや、Swatchで対応していました。
しかし、これからはNorikraが断然お薦めです。論より証拠「こんなに簡単に設定できていいの?」と実感していただけると思います。

動作確認のためテストのQueryを作成する

まずは動作確認のために、1分間隔で同じIPによるアクセスが5回以上あるhostの情報を出力します。

とりあえずQuery name「5 COUNT」、グループ「nginx」としておいてください。

テストクエリ

SELECT host, status, COUNT (status) AS count
FROM nginx.win:time_batch (1 min)
GROUP BY status, host
HAVING COUNT (status) > 5

出力イベント

[1449989603,{"host":"116.0.247.133","count":6,"status":"200"}]
[1449989603,{"host":"59.146.3.222","count":7,"status":"200"}]
[1449989603,{"host":"101.111.73.127","count":28,"status":"200"}]
[1449989603,{"host":"1.75.209.170","count":6,"status":"200"}]

しっかり出力されています。

ここで1点疑問が湧きます。DoS攻撃のしきい値はどれくらいなのでしょうか
まず、サーバリソースを食いつくすようなDoS攻撃は間違いなく遮断する必要があります。
そうは言っても、あまり厳しくしてヘビーユーザーを閉めだすのは避けなければなりません。

間違いなく異常なアクセス数」と判定するには長時間の計測が有効です。
と、こんな時こそ、fluentdGrowthForecastでグラフ化したデータの出番です。
ステータスコード200のアクセスを見てみます。グラフのavgmaxを見れば、どれくらいが正常な値なのかがわかります。

1時間の計測で、1min単位だとmaxが約590、avgが約28とのこと。
1週間の計測で、30min単位だとmaxが約180、avgが約23とのこと。

この数値は全IPを対象としているので、個別IPのアクセス数ではありません。しかし、おおよその正常値はわかりました。
ということで、1分単位の場合、1つのIPで1000アクセスを超える状態が異常と言えそうです。
また、解説の都合上、1分で100アクセスの場合も追加します。

実際には複数のIPによるスローDDoS攻撃も想定して30分で2000アクセスといったQueryも有効だと思います。

1つのIPで1分以内に100アクセスあった場合に出力するクエリ

Query name: 1min_100_count
Group: nginx

SELECT host, status, COUNT (status) AS count
FROM nginx.win:time_batch (1 min, 0L)
GROUP BY status, host
HAVING COUNT (status) > 100

1つのIPで1分以内に1000アクセスあった場合に出力するクエリ

Query name: 1min_1000_count
Group: nginx

SELECT host, status, COUNT (status) AS count
FROM nginx.win:time_batch (1 min, 0L)
GROUP BY status, host
HAVING COUNT (status) > 1000

DoS攻撃のフラグ用として、2つのQueryを登録しました。


NorikraInputでNorikraのイベントをfluentdへ渡す

続いてはNorikraから出力されたイベントをfluentdで受ける設定をします。

# vi /etc/td-agent/td-agent.conf

まずは基本形。Queryごとに取得する方法です。

<source>
	type norikra
	norikra localhost:26571
	<fetch>
		method event
		target 1min_100_count
		tag query_name
		tag_prefix norikra.query
		interval 60s
	</fetch>
</source>

typenorikraは同じです。
<fetch>~</fetch>にデータを取り出す設定を追加します。
コピープラグインのstoreと同じで、並列で複数の設定を追加することができます。

methodeventsweepを指定します。

eventtargetQuery nameを指定し、sweepQueryのgroupを指定します。

tagquery_nameとすると自動でNorikraQuery nameが入ります。

tag_prefixでfluentdのtagを追加します。
上の例で言えば1min_100_countというQuery nameを指定したので、tag_prefixnorikra.queryの後に、1min_100_countが付き「norikra.query.1min_100_count」となります。

他にもtagは「tag string ○○」といった独自のtagを指定することもできます。
また「tag field ○○」とすればNorikrafieldに対応した値が自動で入ります。

intervalは取得する間隔。通知系の場合はNorikraの間隔と合わせます。

Norikraのグループ名で取得する場合

今回は使用しませんが、methodでsweepを指定して、groupがnginxのイベントを取得するサンプルです。

<source>
	type norikra
	norikra localhost:26571
	<fetch>
		method sweep
        	target nginx
		tag field host
		tag_prefix norikra.query
		interval 60s
	</fetch>
</source>

グループ名がnginxのものなので、上の例で言えば2つのQueryを取得することができます。

さらにテストとしてtaghost(IPアドレス)をくっつけてみました。すると以下のように出力されます。

2015-12-13T17:46:39+09:00	norikra.query.1.75.0.225	{"host":"1.75.0.225","count":7,"status":"200"}

norikra.queryの後にIPアドレスがくっついてますね。


NorikraでDoS認定されたIPをメールで通知する

せっかくfluentdNorikraで自動化しているので、この2つで完結するようにfluent-plugin-mailを利用して通知メールを送信します。

詳しい解説はfluent-plugin-mailへどうぞ。

fluent-plugin-mailのインストール

td-agent-gemでインストールします。

# td-agent-gem install fluent-plugin-mail
(省略)
1 gem installed

fluent-plugin-mailの設定

# vi /etc/td-agent/td-agent.conf
<match norikra.query.**>
	type mail
	host localhost
	port 25
	from root
	to root@example.com
	subject SUBJECT: %s
	subject_out_keys tag
	out_keys host,count,status
	# time_locale UTC
</match>

typemailfluent-plugin-mailを指定。

hostはローカルなのでlocalhost。普通にメールサーバを指定しても良い。

portはメールの転送用ポート。ローカルなので25

fromメールの差出人

toメールの宛先。今回はrootからroot@example.com宛に送ります。

subjectタイトル%sは変換指定子
実際に何を出力するかはsubject_out_keysで決める。この場合はtag。そのため
SUBJECT: norikra.query.<host>」と出力されている。

out_keys出力するJSONのkeyを指定する(Norikraでいうfield名)。出力形式は「key名:」という形式になります。
time_locale UTCはオプション。普通にUTF時間で送信するということだと思われます。

では再起動して有効にします。

# /etc/init.d/td-agent restart

後はApache Bench等を利用して1分間に100アクセス以上してみましょう。
正しく設定できていれば以下の様なメールが届きます。

SUBJECT: norikra.query.1min_100_count
root@example.com
2015/12/14 (月) 13:18

host: <IPアドレス>
count: 106
status: 200

今回はローカルのメールサーバへ通知を飛ばしましたが、Gmail等にも対応しています。公式サイトにサンプルが掲載されています。

ということで、Dos攻撃をメールで通知する仕組みができました。


exec Output Pluginを利用して外部スクリプトと連携する

今度はもう1つのフラグを利用して、外部スクリプトを実行する例を紹介します。

特定のイベントで外部スクリプトを実行するには「exec Output Plugin」を利用します。
fluentdで公式にサポートされているので、プラグインを追加する必要はありません。

# vi /etc/td-agent/td-agent.conf

ブロックする「1min_1000_count」用の設定を追加

<source>
        type norikra
        norikra localhost:26571
        <fetch>
                method event
                target 1min_1000_count
                tag query_name
                tag_prefix norikra.block # ブロック用のタグを付ける
                interval 60s
        </fetch>
</source>

exec Output Pluginの設定を追加

<match norikra.block.1min_1000_count>
	type exec
	command /bin/sh /usr/script/dos_block.sh
	format tsv
	buffer_path /var/log/td-agent/buffer/
	keys host
	flush_interval 5s
</match>

NorikraInputtagquery_nameを指定したのでマッチのパターンに利用します。

command実行するスクリプトを指定します。

formatで渡すデータの形式を指定します。この場合はTSV(タブ区切り)を指定しています。他にもJSONやfluentdの高速化を支えているMessagePackなんかも使えます。

buffer_pathでバッファファイルを一時的に保存しておくディレクトリを指定します。(実行してデータを渡した時点で消去されます)

key渡すデータのkeyを指定します。この場合はhostなのでIPアドレスが渡されます。TSV形式の場合は必須です。

flush_intervalイベントを渡されてから5秒毎に実行を指定。マッチするイベントが流れてきてから指定した時間で実行されます。
この場合はNorikraから1分ごとにイベントを渡されるので、それ以上の頻度で外部スクリプトが実行されることはありません

TSV形式の場合はflush_intervalを指定しないと、イベントがバッファされて一定の期間(サイズ)に達するまで実行されないので注意してください。

バッファのサイズはbuffer_queue_limitbuffer_chunk_limitで変更できます。詳しくは公式ページの解説をご覧ください。

fluentdと連携する外部スクリプトを作成

外部ファイルの形式は、他のサイトの解説ではRubyPerlが多いので、このサイトではbashで作成します。
まずは、どんな形でデータが渡されるのか見てみます。

標準入力で何か渡されるだろうということで、テスト用のファイルを作成して、書き出してみます。

# vi /usr/script/dos_block.sh
#!/bin/sh

echo $1 >> /usr/script/test.txt

作成したファイルに実行権限を与えて、結果を書き出すファイル(/usr/script/test.txt)も作成しておきます。

# chmod +x /usr/script/dos_block.sh
# echo -n > /usr/script/test.txt
# chmod 666 /usr/script/test.txt

イベントを意図的に実行し「/usr/script/test.txt」を見てみます。

/var/log/td-agent/buffer/.20151221.q52765e29dea834af.log
/var/log/td-agent/buffer/.20151221.253655e29d45266af.log

どうやら作成されたバッファファイルが渡されているようです。

次はcatを利用してデータを展開してみます。

#!/bin/sh

msg=`cat $1`
echo $msg >> /usr/script/test.txt

再びイベントを発生させて見てみます。すると以下のようにhostの値だけ取得できました。

1.72.5.84 210.250.151.3 126.236.145.205

ここまでわかれば後はお好きな様に、といった感じですね。

拒否しているIPのリストを掲載するファイルを作成

# echo -n > /usr/script/deny_ip
# chmod 666 /usr/script/deny_ip

遮断と通知用のスクリプトを作成

# vi /usr/script/dos_block.sh
#!/bin/sh

MAIL=hoge@example.com
msg=`cat $1`

# TSVの要素を配列に変換
eval ips=("$(sed -e "s/'/'\\\\''/g" -e "s/\t/'\t'/g" -e "s/^/'/" -e "s/$/'/" <<< "$msg")")

# 配列でまわす
for ip in ${ips[@]}; do
	ipaddr=$ip

	# IPアドレスをピリオドで分割
	addr1=`echo $ipaddr|cut -d . -f 1`
	addr2=`echo $ipaddr|cut -d . -f 2`
	addr3=`echo $ipaddr|cut -d . -f 3`
	addr4=`echo $ipaddr|cut -d . -f 4`

	# IPアドレスがプライベートIPアドレスの場合は終了
	if [ "$ipaddr" = "127.0.0.1" ]; then
		exit
	elif [ $addr1 -eq 10 ]; then
		exit
	elif [ $addr1 -eq 172 ] && [ $addr2 -ge 16 ] && [ $addr2 -le 31 ]; then
		exit
	elif [ $addr1 -eq 192 ] && [ $addr2 -eq 168 ]; then
		exit
	fi

	# IPアドレスがホワイトリストに一致する場合は終了
	# この例では100.100.0.0/16を想定(環境によって適宜変更してください)
	if [ $addr1 -eq 100 ] && [ $addr2 -eq 100 ] && [ $addr3 -lt 256 ] && [ $addr4 -lt 256 ]; then
		exit
	fi

	# IPリストにIPが含まれている場合は終了
	file=`cat /usr/script/deny_ip`
	echo "$file" | grep "$ipaddr"
	if [ $? -eq 0 ]; then
		exit
	fi

	# 該当IPアドレスを拒否する設定をiptablesに追加
	# (合わせて30分で削除する設定も追加)
	iptables -I INPUT -s $ipaddr -j DROP
	echo "iptables -D INPUT -s $ipaddr -j DROP" | \
	at now+30minutes > /dev/null 2>&1

	# 該当IPアドレスをdeny_ipに登録
	# (合わせて30分で削除する設定も追加)
	echo "$ipaddr" >> /usr/script/deny_ip
	echo "sed -i "/$ipaddr/d" /usr/script/deny_ip" | \
	at now+30minutes > /dev/null 2>&1

	# アクセス規制IPアドレス情報をメール通知
	echo ; whois $ipaddr | \
	mail -s "$ipaddr DoS attack block" $MAIL
done

IPがプライベートIPの場合ホワイトリストの場合既に遮断リストに掲載されている場合はアクセス規制しないので除外

それ以外のIPはiptablesでアクセスをすぐに遮断し、拒否リストに保存atコマンドで30分経過すると復活

一連の処理をしたことをメールで通知
せっかくなのでfluentdに戻してmailプラグインで…とも考えましたが、あまりに冗長な気がしてやめました。(meilの利用はmailxが必要です。)

といったスクリプトです。

このスクリプトの使用は自己責任でお願いします。

不慣れな方は、まずiptablesの設定をコメントアウトしてから運用してください。最悪の場合、自分のIPが拒否されて接続できなくなることも考えられます

これでfluentdNorikra、更に外部スクリプトを連動させて、DoS攻撃を自動で遮断することができました。
一連の技術を利用すれば「CPUの利用率など、○分平均で○以上の場合、メールで通知する」といった仕組みも簡単に実装できます。

以上のことからわかる通り、fluentdNorikraを組み合わせれば、刻々と構成が変更になる環境でも、柔軟に対応が可能です。まさに鬼に金棒です。
こんなに有用なソフトをオープンソースで提供してしまって良いのでしょうか?変な心配をしてしまいますw

この場を借りて両ソフトの開発に携わった全ての方々にお礼をさせていただきます。

今回の投稿を作成するにあたり参考にさせていただいた「Norikra+FluentdでDoS攻撃をブロックする仕組みを作ってみた」では、Rubyを利用してAWSのNetwork ACLでアクセスを拒否していました。AWSを利用している方はぜひ参考にしてみてください。


情報いただきました。
今回のDoS攻撃をブロックするためのfluentdのプラグインが既に存在するようです。
fluent-plugin-dos_block_acl
奇しくも、このページで紹介したスクリプトの機能はほとんどサポートされています。

「多くの人が必要な機能は、有志によってプラグインとして提供される。」という点はオープンソースならではのメリットですね。
プラグイン作者の方に感謝です。