ビックデータ時代の柔軟なサーバ管理方法
前回の投稿でNorikraのQueryについて学んだので、今回は実用的な例でQueryを作成してみます。
Nginxのログを取得し、DoS攻撃らしき兆候を察知したらメールで通知。
さらにDoS攻撃と認定したら、即アクセスを遮断するという仕組みを実装します。
目次
- fluentdとNorikraの連携について
- NorikraOutputでfluentからNorikraへイベントを渡す
- nginxのaccess.logでDoS攻撃を察知する
- NorikraInputでNorikraのイベントをfluentdへ渡す
- NorikraでDDoS認定されたIPをメールで通知する
- exec Output Pluginを利用して外部スクリプトと連携する
- fluentdと連携する外部スクリプトを作成
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_stringでNorikraのTarget名を指定。
必要な設定は上記の3つだけです。
remove_tag_prefixで他のプラグインと同じように不要なtagを削除。
target_map_tagを有効にするとfluentdのTagがそのままNorikraのTargetに変換される。上記の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攻撃は間違いなく遮断する必要があります。
そうは言っても、あまり厳しくしてヘビーユーザーを閉めだすのは避けなければなりません。
「間違いなく異常なアクセス数」と判定するには長時間の計測が有効です。
と、こんな時こそ、fluentdとGrowthForecastでグラフ化したデータの出番です。
ステータスコード200のアクセスを見てみます。グラフのavgとmaxを見れば、どれくらいが正常な値なのかがわかります。
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>
typeとnorikraは同じです。
<fetch>~</fetch>にデータを取り出す設定を追加します。
コピープラグインのstoreと同じで、並列で複数の設定を追加することができます。
methodはeventかsweepを指定します。
eventはtargetにQuery nameを指定し、sweepはQueryのgroupを指定します。
tagはquery_nameとすると自動でNorikraのQuery nameが入ります。
tag_prefixでfluentdのtagを追加します。
上の例で言えば1min_100_countというQuery nameを指定したので、tag_prefixのnorikra.queryの後に、1min_100_countが付き「norikra.query.1min_100_count」となります。
他にもtagは「tag string ○○」といった独自のtagを指定することもできます。
また「tag field ○○」とすればNorikraのfieldに対応した値が自動で入ります。
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を取得することができます。
さらにテストとしてtagにhost(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をメールで通知する
せっかくfluentdとNorikraで自動化しているので、この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>
typeにmailでfluent-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>
NorikraInputでtagにquery_nameを指定したのでマッチのパターンに利用します。
commandで実行するスクリプトを指定します。
formatで渡すデータの形式を指定します。この場合はTSV(タブ区切り)を指定しています。他にもJSONやfluentdの高速化を支えているMessagePackなんかも使えます。
buffer_pathでバッファファイルを一時的に保存しておくディレクトリを指定します。(実行してデータを渡した時点で消去されます)
keyで渡すデータのkeyを指定します。この場合はhostなのでIPアドレスが渡されます。TSV形式の場合は必須です。
flush_intervalでイベントを渡されてから5秒毎に実行を指定。マッチするイベントが流れてきてから指定した時間で実行されます。
この場合はNorikraから1分ごとにイベントを渡されるので、それ以上の頻度で外部スクリプトが実行されることはありません。
TSV形式の場合はflush_intervalを指定しないと、イベントがバッファされて一定の期間(サイズ)に達するまで実行されないので注意してください。 バッファのサイズはbuffer_queue_limitやbuffer_chunk_limitで変更できます。詳しくは公式ページの解説をご覧ください。
fluentdと連携する外部スクリプトを作成
外部ファイルの形式は、他のサイトの解説ではRubyやPerlが多いので、このサイトでは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が拒否されて接続できなくなることも考えられます。
これでfluentdとNorikra、更に外部スクリプトを連動させて、DoS攻撃を自動で遮断することができました。
一連の技術を利用すれば「CPUの利用率など、○分平均で○以上の場合、メールで通知する」といった仕組みも簡単に実装できます。
以上のことからわかる通り、fluentdにNorikraを組み合わせれば、刻々と構成が変更になる環境でも、柔軟に対応が可能です。まさに鬼に金棒です。
こんなに有用なソフトをオープンソースで提供してしまって良いのでしょうか?変な心配をしてしまいますw
この場を借りて両ソフトの開発に携わった全ての方々にお礼をさせていただきます。
今回の投稿を作成するにあたり参考にさせていただいた「Norikra+FluentdでDoS攻撃をブロックする仕組みを作ってみた」では、Rubyを利用してAWSのNetwork ACLでアクセスを拒否していました。AWSを利用している方はぜひ参考にしてみてください。
情報いただきました。
今回のDoS攻撃をブロックするためのfluentdのプラグインが既に存在するようです。
「fluent-plugin-dos_block_acl」
奇しくも、このページで紹介したスクリプトの機能はほとんどサポートされています。
「多くの人が必要な機能は、有志によってプラグインとして提供される。」という点はオープンソースならではのメリットですね。
プラグイン作者の方に感謝です。