AWS Lambda + AWS WAF でDOS攻撃からの ✝守護者✝ を実装する
はじめに
DOS攻撃からいかにシステムを守るか。オートスケールの仕組みが充実し、大量のアクセスでも耐えられるようになってきている昨今ですが、悪意ある急激なアクセス増加は誰も望まないはずです。防御する仕組みが欲しいところではありますが、開発の時間が限られている中で、なかなか防護機構の自動化まで持っていくのは骨が折れるのではないかと思います。そこで今回は、AWSを使ったシステムを構築するシーンで、DOS攻撃から自動でWAFを設定する仕組みを作ります。重視した点は以下2点です。
- 安価であること。可能な限り新しいAWSリソースを使わない、使うとしても高価なものは避ける。
- ポータビリティが高いこと。別の環境に同様の手順で導入できること。
DOS攻撃防御の仕組み比較
Apache の mod_dosdetector
アプリケーションサーバ群の前段にApacheサーバを置き、そこにmod_dosdetectorを導入することを考えます。
長所
- ミドルウェアのモジュールとして実装されており、導入実績がある
- 設定ファイルさえ記載すればすぐに導入できる
考慮すべきこと
- Apacheサーバに負荷が集中し、AWSの Auto Scaling の思想と相性が悪い
- しきい値の修正には設定ファイルを修正したあとApacheのgracefulが必要となり、運用ハードルがやや高い
- Apacheに到達するまでは攻撃によりリソースを消費することになる。ネットワーク帯域の飽和などには注意が必要
Amazon KinesisとAWS WAFを利用する方法
弊社鈴木が作成した手法です。
長所
- Amazon Kinesis Streams を利用することによる圧倒的リアルタイム性
- Amazon Kinesis Firehose, Analytics, Streams の知見がたまる
- AWS WAFの自動設定だけでなく、分析結果から様々なアクションにつなげることができる
考慮すべきこと
- コスト面
- Amazon Kinesis Firehose, Analytics, Streams の知見が必要になる
本記事で実装する仕組み
長所
- 利用するAWSリソースは Amazon S3, AWS Lambda, AWS WAF だけであり、コスト面に優れる
- アクセスログがS3に出力できさえできれば、環境を問わない
考慮すべきこと
- ブロックするまでラグがある。具体的な程度は、fluentd の転送タイミングの設定による
- 途中、分析工程を挟まないので、S3に出力したログはDOS攻撃ブロック以外の用途として期待できない
ApacheをはじめとしたHTTPサーバの機能を用いるのでなく、 AWS WAF を使うメリットは、EC2インスタンス到達さえ許さず、CloudFrontの段階でアクセスを遮断できる点です。
構成
鈴木が考案した方法から、リアルタイム性を若干犠牲にして Amazon Kinesis Firehose, Analytics, Streams をとっぱらった構成です。
- CloudFront と AWS WAF を導入します
- アプリケーションサーバにnginxとfluentdを導入し、アクセスログ(X-Forwarded-Forを含む)がS3バケットに連携されるようにします
- Lambda Functionにおいて、
--> S3バケットへのアクセスログPUTを契機として Lambda Function が起動するようにします
--> アクセスログ中の特定IPの数がしきい値を超えていた場合、AWS WAF のAPIをコールしてブラックリストIPを追加します
--> IPをブロックしたことをSlackなどで担当者に通知します
順番に詳しく見ていきましょう。
CloudFront と AWS WAF を導入する
これらがないと始まりません。早速導入しましょう。
CloudFrontを作成する
特に難しいことはないと思います。Route53などを使ってもともとALB/ELBへアクセスを振り分けていた場合、CLoudFrontへ向くように修正してください。
AWS WAF を作成する
Step1: Name web ACL
web ACL を作成します。 AWS resource to associate
で先に作成した CloudFront と関連付けます。
Step2: Create conditions
Create conditions で IP match conditions を選択します。
IPアドレスは今は空でOKです。
Step3: Create rules, Step4: Create
次にRuleを作ります。
これで完成です。この時点で仕組みはもう半分完成しています。
作成されたブラックリストとそのID
いま作成した IP adresses に対してIPアドレスを追加すると、ブラックリスト判定され、CloudFrontにてアクセスがブロックされます。要はこの作業を手ではなく Lambda Funtion にやらせてやろう、というのがこれから先で述べる内容です。
アプリケーションサーバにnginxとfluentdを導入する
それでは Lambda Function を作っていきます。まずはINPUTとなる素材を用意してやる必要があります。アクセスログです。nginx+fluentdでいきます。nginxはApacheでも構いません(実際どちらでも動作を確認できました)。とにかくここでやりたいことは、
- HTTPサーバの機能を使って X-Forwarded-For を含むアクセスログを出力する
- fluentdを使って、アクセス元のIPが記録されたログをS3に転送する
これら2点です。動機を説明しなければならないことがふたつ。
X-Forwarded-For を出力する手段はアプリケーションフレームワークにまかせてもよいのでは?
その通りです。ただ、私は「ポータビリティ」を重視しました。フレームワークの機能を使う場合、都度アクセスログの出力について調べなければなりません。アプリケーションとして利用する言語やフレームワークが変わったとしても、「HTTPサーバによるアクセスログ出力」できるレイヤがあることで、違いを吸収できるというメリットがあります。
なぜ X-Forwarded-For が必要なの? ALB/ELB のアクセスログでも良いのでは?
ALB/ELBのアクセスログは IP address の出力にしか対応しておらず、この値は、ALB/ELBの直前に居る CloudFrontのものになってしまうからです。CloudFront のアクセスを遮断してしまっても、意味がありません。よって、本当のアクセス元を知るために、EC2インスタンスのHTTPサーバで X-Forwarded-For が必要になります。
具体的な設定内容を見ていきます。
nginx
アプリケーションログを出力するための最小限の設定でOKです。アプリケーションサーバに PlayFramework を利用している環境では、以下のように設定しました。
http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '"$http_x_forwarded_for" - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; upstream play { server 127.0.0.1:9000; } include /etc/nginx/conf.d/play.conf; }
log_format
で http_x_forwarded_for
を出力している点に注目してください。これがnginxの大事な仕事です。
出力されるログは以下のようになります。
"xxx.xxx.xxx.xxx, 54.239.196.134" - - [13/Jan/2017:08:21:19 +0000] "GET /healthcheck HTTP/1.1" 200 33 "-" "Go-http-client/1.1"
fluentd
先でnginxが出力したログをS3バケットに送信します。
<source> type tail path "/var/log/nginx/access.log" pos_file "/var/log/td-agent/nginx.log.pos" tag log.play.access format /^"(?<x-forwarded-for>[^\"]*)" [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$/ time_format %d/%b/%Y:%H:%M:%S %z </source> <filter log.play.access> type record_transformer enable_ruby true renew_record true <record> host ${record["x-forwarded-for"].split(',')[0]} </record> </filter> <match log.play.access> type s3 s3_bucket "play-access-log" path "play/access/" s3_object_key_format %{path}%{time_slice}/access_%{index}.%{file_extension} buffer_path /var/log/td-agent/s3/play_access time_slice_format %Y/%m/%d/%H time_slice_wait 1m utc buffer_chunk_limit 512k format json include_time_key true time_key log_time </match>
- source: アクセスログをパースしています。
- filter: 今回に関しては、ここでアクセスログをスリム化し、IPブロックのための必要最小限の情報に切り捨てています。S3の利用領域節約と、AWS Lambda で利用するメモリが多くなりすぎないようにする配慮です。
- match: S3に転送しています。
time_slice_format
とbuffer_chunk_limit
を見てください。この設定で、「一時間経過したらS3バケットにパースしたログを転送する。もし、ログのサイズが512KBに達した場合、一時間を待たずに転送する」という動きになります。攻撃を受けている場合はログサイズが大きくなり後者が適用されるでしょうから、 AWS Lambda としては512KB中に含まれているログから攻撃とみなすIPアドレスを選別することになります。
AWS Lambda
Lambda Function をデプロイする
コードは GitHub にコミットしました。
apexを用いてデプロイします。
apex deploy attack-guardian-play --env stg
あっという間に Lambda Function のできあがりです。以後修正が入った場合も同じコマンドで上書きがかけられます。
コードの解説は長くなるので割愛しますが、やっている仕事は以下です:
- apexの機能を用いてjsonファイルから環境設定を読み出し、 AWS Lambda の環境変数にセットする
- S3にアクセスログがPUTされたら、 Lambda Function としては、まずセットされた環境変数を読み出す
- アクセスログの内容を読み込み、IPアドレス出現数で集計する
- 環境変数にセットされたしきい値を基準にブラックリスト判定するかどうか決める
- ブラックリスト判定対象のIPアドレスがあった場合、
- AWS WAF のAPIを呼び出してブラックリストにIPアドレスを追加する(この時点で防御完了)
- SNSトピックARNに対してメッセージをPublishし、Subscriberにブロックしたことを通知する
- Slackのincomming hook urlにリクエストを送り、ブロックしたことを通知する
S3バケットのPUTをトリガーに起動する
以下のように設定します。
ためす
ベジータを使って同一のアクセス元から攻撃をかけてみます。
vegeta attack -rate=50 -duration=300s -targets=target.txt
通知が飛んできました!
↓Slack通知
↓メール通知
ブラウザからアクセスしてみると、確かにCloudFrontのところでブロックされているようです。
おわりに
導入を検討する場合、以下の点にご注意ください。
- AWS WAF は CloudFront に対して適用されるものであるため、CloudFront の導入が必須です。
- 残念ながら ALB/ELB のアクセスログは利用できません。IP Address の出力しか対応しておらず、この値は必ず CloudFront の値になってしまうためです。アクセス元のIPアドレスを出力するために、アプリケーションレイヤのログ出力機能を使って X-Forwarded-For を出力する必要があります。
これらの条件を満たせば、安価で手軽にDOSからの防護機構を実装することができます。これだけ安価で簡単に導入できるようになったエコシステムへ感謝するとともに、攻撃に怯えず夜はぐっすり眠ってよりよいエンジニアリングライフを送りましょう!
参考
- How to Configure Rate-Based Blacklisting with AWS WAF and AWS Lambda | AWS Security Blog
- アクセスログ - Amazon CloudFront
- apex/apex: Build, deploy, and manage AWS Lambda functions with ease (with Go support!).