nginxでDDoS(?)対策

投稿日:

なんか知りませんが、3月に入ったら業務で管理してるサーバに妙なアクセスがいっぱい来るようになりました。

HEAD なんとか

とリクエストしてきて、これが成功(200)すると即

GET なんとか

で取得、というのを短時間で繰り返す→放っとくとセッションとデータベースアクセスが爆増してえらいことに...。

最初のHEADに失敗(403)すると後のGETは来ず、以後は低めの頻度でHEADリクエストを投げてくるに留まるようになる。

アクセス元IPを見るとクラウドとか中国とかベトナムとか...延べ数千くらい。

なんかDDoSというにはアクセス頻度が中途半端(数秒の間隔が空くことが多い)なんですが、アクセスしてくるurlに特徴的なシグネチャがあり、通常のアクセスとは区別できそう。

「HEADリクエストを全部弾く」も考えたけど、攻撃以外のHEADリクエストも全く無いわけではない→まずHEADリクエストのログを切り分け、そこから攻撃の特徴的シグネチャを探す方法で「弾く対象」を決めます。

まぁ「攻撃じゃない」HEADといっても、その殆ど全てが

128.24.92.243 - - [22/Mar/2025:06:44:18 +0900] "HEAD /new HTTP/1.1" 404 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" "-" 0.002 http
128.24.92.243 - - [22/Mar/2025:06:44:18 +0900] "HEAD /main HTTP/1.1" 404 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" "-" 0.003 http
128.24.92.243 - - [22/Mar/2025:06:44:19 +0900] "HEAD /home HTTP/1.1" 404 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" "-" 0.003 http

みたいな「攻撃の予備行動」だったりするんでお察しではある(^_^;)

攻撃を受けている(?)サーバにはフロントエンド(兼SSLアクセラレータ)としてnginxのリバースプロキシを噛ませてある→こいつで対策してみます。

nginxにはngx_http_limit_req_moduleという「リクエストの処理速度を制限する」モジュールもあるのですが、今回は「すげぇ高密度のバーストアクセスを弾く」という感じじゃないので自動deny列記=ブラックリスト方式でやってみる。

とりあえず、nginxのリバースプロキシを定義してるproxy.conf(←自作ファイルでありファイル名は人によって違う)に

limit_except GET HEAD POST { deny all; }

include /etc/nginx/blacklist/*.conf;

を書き足し、GET/HEAD/POST以外のアクセスを拒否+ブラックリストを読み込む設定にします。

mkdir /etc/nginx/blacklist/

でブラックリストを置くディレクトリも作ります。

続いてnginxのログから「HEAD」リクエストを含む行を抽出。

grep ' \"HEAD /' /var/log/nginx/access.log > /tmp/head_access.log

このやり方だと「ログローテーションしたタイミング」でブラックリストが空になりしばらく無防備になってしまう→HEADリクエストの成功が増え雪だるま式にアクセスが増えてしまうので、考えます。

nginxのログ見ると

-rw-rw-r-- 1 nginx root 8342131 3月 26 09:02 access.log
-rw-rw-r-- 1 nginx root 934963 2月 25 07:21 access.log-20250225.gz
-rw-rw-r-- 1 nginx root 23859954 3月 4 03:26 access.log-20250304.gz
-rw-rw-r-- 1 nginx root 19800557 3月 11 03:21 access.log-20250311.gz
-rw-rw-r-- 1 nginx root 7180388 3月 18 03:27 access.log-20250318.gz
-rw-rw-r-- 1 nginx root 65616225 3月 25 03:36 access.log-20250325

「ひとつ古いログ」は圧縮されず残ってるみたい→このファイルも使って最大2週間分のログから抽出するようにすればログローテーションしても「ブラックリストがすっからかん」になるタイミングが発生しなさそう。

「ひとつ古いログ」は「access.log-*」というファイル名で「*.gz」でないファイルなので

find /var/log/nginx/ -type f -name "access.log-*" -not -name "*.gz"

というコマンドで見つかる→見つかったファイルをgrepし、更に現在のログからgrepした
結果を足す、ということで

find /var/log/nginx/ -type f -name "access.log-*" -not -name "*.gz" | xargs grep ' \"HEAD /' > /tmp/head_access.log

grep ' \"HEAD /' /var/log/nginx/access.log >> /tmp/head_access.log

として無事「2週分のHEADリクエストログ」を得ることができました(^_^)

こうして得られたHEADリクエストのログから「ワタシが攻撃したと判定する」IPを抽出→deny列記のファイルを生成。

grep '%5C%5C%5C%5C%5C%5C%5C%5C%5C%5C%5C%5C%22' /tmp/head_access.log | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' -o | sort | uniq | while read line; do echo "deny ${line};"; done> /etc/nginx/blacklist/blacklist3.conf

(この他にもいくつか「ヤバそうなHEADリクエストを送ってきたやつ」を同様の手法で抽出している)

あとはシェルスクリプトの末尾に

/usr/sbin/nginx -s reload

と書き、cronを使って短い間隔で実行し続けることで

deny 14.232.142.131;
deny 14.232.149.106;
deny 14.232.151.90;
deny 14.232.158.96;
deny 14.232.173.8;
deny 14.232.176.222;

みたいなconfファイルが生成され、以後このIPからのリクエストをdeny(403)するようになります。

HEAD→GETのコンビネーションは、ワタシの技量ではPHPとか使わないと特定できなかった。まぁログを読み込めば要素ごとに分割し上の行と比較するだけ→大したプログラムではない。

リバースプロキシが弾くのでバックエンドのwebサーバやデータベースには何も届かず、リバースプロキシ自体もバックエンドを待つことなく即切断→殆ど負荷になってません。

攻撃(?)してくるアクセス元IPは多いですが、一度でも攻撃的リクエストを送ってくると自動でdenyされていき、割とすぐに「重い」状態を脱することができました。

攻撃(?)元、かなりはっきりと「IPを使い捨ててアクセスしてる」んですが、それでも「一度攻撃してきたら拒否」には一定の効果があるみたい。

250326_01.jpg

アクセス元が多すぎ「疑わしきは全部deny」方針であれば

grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.' -o | sort | uniq | while read line; do echo "deny ${line}0/24;"; done

とかしておけば一度に256個のIPをまとめてdeny出来ますが、ワタシの例ではサブネットマスク255.255.255.0くらいの幅ではあんまりブラックリストの行数変わりませんでした。

deny 47.242.148.0/24;
deny 47.242.149.0/24;
deny 47.242.160.0/24;
deny 47.242.167.0/24;

思い切って255.255.0.0(65,536個)まで広げると予防効果抜群な上にガッツリとブラックリスト小さくなりますが、これは流石に巻き込み範囲が広すぎる気がする→お好みで。

deny 222.253.0.0/16;
deny 8.210.0.0/16;