FluentdとNorikraでDoS攻撃検知→Fluentdでexec Output Pluginを使用してメール通知

##はじめに
最近、DoS攻撃検知させる案件があり、
FluentdとNorikraで検証したときの構成を投稿します。

##参考
以下サイトを参考にさせていただきました。
https://dev.classmethod.jp/cloud/aws/block_dos_attack_by_norikra/

##インストール環境
- CentOS 6
- Apache access_log
- Fluentd (v1.0 td-agent3)
- jdk 1.8
- rbenv
- jruby
- Norikra 

##構築
###rbenvインストール
```
yum install -y git gcc-c++
git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
exec $SHELL -l

#確認
rbenv --version
```
###JRubyインストール
今回はとりあえず最新で jruby-9.2.0.0 をインストールしました。

```
git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
rbenv install -l|grep jruby
rbenv install jruby-9.2.0.0
rbenv global jruby-9.2.0.0

#確認
ruby -v
```

###Norikraインストール
```
gem install norikra --no-ri --no-rdoc
rbenv rehash
mkdir /{etc,var/log,var/run}/norikra

#確認
which norikra
gem list --local | grep norikra

```
####Norikra Initスクリプト作成
@Kedamariさんのものを参考にさせていただきました。
https://qiita.com/Kedamari/items/6ee6252025ca6ddc6b21

####環境変数系の設定
```
# 環境変数系の設定
cat <<'EOT' > /etc/sysconfig/norikra
NORIKRA_OPT="-Xmx512m -d"
EOT
```
####Initスクリプト
起動ユーザはrootに変えています。
※サボってます、、

```
cat <<'EOT' > /etc/init.d/norikra
#!/bin/bash
#
# Norikra init shellscript
#   Copyright 2015, Kedamari
#
# /etc/rc.d/init.d/norikra
#
# chkconfig: - 80 20
# description: norikra
# processname: norikra
# pidfile: /var/run/norikra/norikra.pid

##########################
### Disabled http_proxy
##########################
unset http_proxy
unset HTTP_PROXY

##########################
### Definition
##########################
export PATH=/sbin:/usr/sbin:/bin:/usr/bin

NORIKRA_NAME=Norikra
NORIKRA_HOME=/etc/norikra
NORIKRA_DEFAULT=/etc/sysconfig/norikra

NORIKRA_USER=root
NORIKRA_GROUP=root

NORIKRA_LOG_DIR=/var/log/norikra
NORIKRA_BIN_FILE=/root/.rbenv/shims/norikra
NORIKRA_PID_FILE=/var/run/norikra/norikra.pid
NORIKRA_LOCK_FILE=/var/run/norikra/norikra.lock
NORIKRA_STATS_FILE=${NORIKRA_HOME}/norikra.json

. "${NORIKRA_DEFAULT}"
NORIKRA_OPTIONS="start ${NORIKRA_OPT} --pidfile=${NORIKRA_PID_FILE} --stats=${NORIKRA_STATS_FILE} --logdir=${NORIKRA_LOG_DIR} --middle"

START_STOP_DAEMON_ARGS="-l ${NORIKRA_USER} -g ${NORIKRA_GROUP}"

# timeout can be overridden from /etc/sysconfig/td-agent
STOPTIMEOUT=120
RETVAL=0

# Source function library.
. /etc/init.d/functions

##########################
### Function that starts
##########################

do_start() {
  ulimit -n 65536 1>/dev/null 2>&1 || true
  echo -n "Starting ${NORIKRA_NAME}: "
  local RETVAL=0
  runuser ${START_STOP_DAEMON_ARGS} -c bash -c "${NORIKRA_BIN_FILE} ${NORIKRA_OPTIONS}" || RETVAL="$?"
  [ $RETVAL -eq 0 ] && success && touch "${NORIKRA_LOCK_FILE}" || failure
  echo
  return $RETVAL
}

##########################
### Function that stops
##########################
do_stop() {
  echo -n "Shutting down ${NORIKRA_NAME}: "
  local RETVAL=0
  if [ -e "${NORIKRA_PID_FILE}" ]; then
    # Use own process termination instead of killproc because killproc can't wait SIGTERM
    NORIKRA_PID=`cat "${NORIKRA_PID_FILE}" 2>/dev/null`

    ${NORIKRA_BIN_FILE} stop >/dev/null 2>&1 || RETVAL="$?"
    if [ $RETVAL -eq 0 ]; then
   TIMEOUT="$STOPTIMEOUT"
   while [ $TIMEOUT -gt 0 ]; do
     ${NORIKRA_BIN_FILE} stop >/dev/null 2>&1 || break
     sleep 1
     let TIMEOUT="${TIMEOUT}-1" || true
   done
   if [ "$TIMEOUT" -eq 0 ]; then
     echo -n "Timeout error occurred trying to stop ${NORIKRA_NAME}..."
     RETVAL=1
     failure || true
   else
     RETVAL=0
     success
   fi
    else
   failure || true
    fi
  else
    failure || true
    RETVAL=4
  fi

  echo
  [ $RETVAL -eq 0 ] && rm -f "${NORIKRA_PID_FILE}" && rm -f "${NORIKRA_LOCK_FILE}"
  return $RETVAL
}

##########################
### Function that restarts
##########################
do_restart() {
  do_stop || true
  do_start
}

case "$1" in
"start" )
  test -f ${NORIKRA_LOCK_FILE} || false
  do_start
  ;;
"stop" )
  do_stop
  ;;
"restart" )
  do_restart
  ;;
"status" )
  status -p "${NORIKRA_PID_FILE}" "${NORIKRA_NAME}"
  ;;
* )
  echo "Usage: $0 {start|stop|restart|status}" >&2
  exit 1
  ;;
esac
EOT
```

####自動起動
```
chkconfig --add norikra
chkconfig norikra on
chmod +x /etc/init.d/norikra
```
####Norikraクエリ設定
例です。SQL文は初心者なのですみません。

- 画像、CSS、JSなどは対象外
- プライベートIPからのアクセスは除外
- 1分間で60以上アクセスで検知

```
select CURRENT_TIMESTAMP() as timestamp,
  host,
  count(*) as requests
from access.win:time_batch(1 min)
where not (
  path like '%.gif'  or
  path like '%.jpg'  or
  path like '%.jpeg' or
  path like '%.png'  or
  path like '%.JPG'  or
  path like '%.ico'  or
  path like '%.js'   or
  path like '%.css'  or
  path like '%.svg'  or
  path like '%.woff'
  ) and not (
  host like '10.%'      or
  host like '172.16.%'  or
  host like '172.17.%'  or
  host like '172.18.%'  or
  host like '172.19.%'  or
  host like '172.2%'    or
  host like '172.30.%'  or
  host like '172.31.%'  or
  host like '192.168.%'
  )
group by host
having count(*) >= 60
```


###Fluentdインストール

公式サイト手順
https://docs.fluentd.org/v1.0/articles/install-by-rpm

```
curl -L https://toolbelt.treasuredata.com/sh/install-redhat-td-agent3.sh | sh
```
####プラグインインストール
今回使うのは、fluent-plugin-norikraだけです。

```
td-agent-gem install fluent-plugin-norikra
```

####Initスクリプト調整
またサボってます、、

```
# 起動ユーザをrootに変更
vi /etc/init.d/td-agent
TD_AGENT_USER=root
TD_AGENT_GROUP=root
```
####自動起動
```
chkconfig td-agent on
```
####Fluentd設定
ログフォーマットはApacheのcombinedに％Dをつけたもの

```
LogFormat "%h %l %u %t \"%r\" %>s %b %D \"%{Referer}i\" \"%{User-Agent}i\""
```


```
cp -p /etc/td-agent/td-agent.conf /etc/td-agent/td-agent.conf_org &&\
cat <<'EOT' > /etc/td-agent/td-agent.conf
#### Global Setting
<source>
  @type monitor_agent
  bind 0.0.0.0
  port 24220
</source>

#### Input
### Apache
<source>
  @type tail
  tag apache.access
  format /^(?<remote_host>[^ ]*) (?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*) (?<response_time>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$/
  time_format %d/%b/%Y:%H:%M:%S %z
  types code:integer,size:integer,response_time:float
  path /var/log/httpd/access_log
  pos_file /var/log/td-agent/apache.access.pos
  read_from_head true
</source>

###norikra
<source>
  @type norikra
  norikra localhost:26571
  <fetch>
    method sweep
    tag query_name
    tag_prefix norikra.query
    interval 1s
  </fetch>
</source>

#### Output
### Apache
<match apache.access>
  @type norikra
  norikra localhost:26571
  buffer_queue_limit 1
  retry_limit 0
  remove_tag_prefix apache
  target_map_tag true
  <buffer>
    @type file
    path /var/log/td-agent/buffer/apache.access
    flush_interval 1s
  </buffer>
</match>

### ExecuteCommand
<match norikra.query.**>
  @type exec
  command /bin/bash /usr/local/bin/action.sh
  <format>
    @type  json
    keys timestamp,host,requests
  </format>
  <inject>
    time_key timestamp
    time_format %Y-%m-%dT%H:%M:%S
    timezone Asia/Tokyo
  </inject>
  <buffer>
    @type file
    path /var/log/td-agent/buffer/norikra.query
    flush_interval 1s
  </buffer>
</match>
EOT
```

###jqコマンドインストール
exec Output Pluginでjson形式のファイルをシェルスクリプトに渡します。
jqを使うとパースがとても簡単なので入れます。

```
# ここからダウンロード
https://stedolan.github.io/jq/

# とりあえずダウンロードしたものをPATHの通っているところに置く
mv jq-linux64 /usr/bin/jq
chmod +x /usr/bin/jq
```

###exec output pluginで実行されるスクリプト
Fluentdからは目的のjsonが複数行渡されることもあるので、
一応すべての行を読み取るようにしてあります。

```
#!/bin/bash

logdir=/usr/local/bin
logfile=action.log

mail_server=localhost
mail_port=25
mail_from=norikra@example.com
mail_subject="題名入力"
mail_to[0]=user1@example.com
mail_to[1]=user2@example.com
mail_msg=$(cat <<EOT

・・・メール本文・・・

EOT
)

{
# Parse Json
mapfile -t msg < <(cat "$1")

for ((i=0; i<${#msg[@]}; i++)); do
  if [ $i -gt 0 ]; then
    mail_body+="\n"
  fi
  host="$(echo "${msg[$i]}" | jq -r '.host')"
  requests="$(echo "${msg[$i]}" | jq -r '.requests')"
  timestamp="$(echo "${msg[$i]}" | jq -r '.timestamp')"
  request_data+="検知日時: $timestamp, 送信元IP: $host, リクエスト数: $requests"
done

# ログ出力
echo -e "$request_data"

# メール本文合成
mail_body=$request_data
mail_body+=$mail_msg


# Send Mail
mail_tos=""
for ((i=0; i<${#mail_to[@]}; i++)); do
  mail_tos+="${mail_to[$i]} "
done

echo -e "$mail_body" | mail -S "smtp=smtp://$mail_server:$mail_port" \
-s "$mail_subject" -r "$mail_from" "$mail_tos"

} 2>&1 | awk '{print strftime("%Y-%m-%dT%H:%M:%S "),$0 } { fflush() } ' >> "${logdir}/${logfile}"
```
※execプラグインでは、実行するコマンドに引数として、バッファファイルが指定されて実行されます。
例えば今回の場合、以下コマンドをFluentdが実行します。

```
/bin/bash /usr/local/bin/action.sh /var/log/td-agent/buffer/norikra.query/<バッファファイル>
```