悪意あるボットは止められない。代わりにアプリ前で切り捨てよう - nginxで奇妙なURLを整理する方法

nginxのblackhole.confで奇妙なURLを整理する

ウェブアプリケーションをインターネットに公開すると、どんなフレームワークを使っても奇妙なリクエストが飛び交います。

  • 存在しない/wp-admin/
  • PHPを一行も書いていないのに/index.php/xmlrpc.php
  • .git.env/cgi-bin/などの機密ファイル/ディレクトリ
  • wso.phpshell.phptest.php

自分で作ったサービスではなく、世界中のスキャナやボットが「脆弱性があるか?」と試すURLです。正直言って、これに当たらないサーバーは世界に存在しないと言っても過言ではありません。

幸い、アプリケーションレベルで防御体制が整っていれば、ほとんどは400/404で処理されます。問題はその後です。

  • ログが汚れます。 本当のユーザーリクエストを見る前に、奇妙なリクエストでスクロールの半分を占めます。
  • ほんの少しですがCPUを消費します。 「安全だから大丈夫」と言い切れず、不要な計算をしている感覚が不安です。
  • 心が疲れます。 ログを見るたびに/wp-login.phpが数百行…見るだけで疲労感が蓄積します。

そこで私はこうしたものをアプリ前のnginxで一気に切り捨てます。 アプリに到達させず、ホストレベルで「ブラックホール」へ送るのです。

この記事では:

  • なぜこの「nginxでの一次ブロック」が有効なのか
  • blackhole.confのようなファイルで共通ブロックルールを管理する方法
  • 実際に使えるnginx設定サンプル

をまとめます。


1. アプリケーションだけでブロックすると残念な理由



私たちがよく行う基本的なアプローチは次のとおりです。

  • ルーター/コントローラにないURL → 404
  • 例外処理 → 400/500
  • ログ → APM / log collectorで収集

機能的には問題ありません。しかし運用観点ではいくつかの不満点があります。

  1. ログノイズ * APMやロギングシステムでエラー/404の比率が実際のユーザーよりもスキャナ比率で膨らみます。 * 「これが本当のユーザーか、スキャナか」を毎回目で区別しなければなりません。

  2. 層が下すぎてブロック * リクエストがアプリまで来たということは、 * フレームワーク・ミドルウェア・ルーティング層をすべて通ったことを意味します。 * もう一度考えると、「ここまで来させる必要があるのか?」という疑問が湧きます。

  3. 微細だが累積するリソース消費 * 一度か二度なら問題ありませんが、 * 24時間通して来るスキャナトラフィックが累積するとかなりのリクエスト数を占めます。

そこで私はこうした「見ても役に立たないリクエスト」を上位レイヤーでできるだけ整理し、入ってくるのを抑えることを好みます。


2. nginxでブラックホールを作る: 444で即切断

nginxにはHTTP標準にない独自ステータスコードが一つあります。

return 444;
  • レスポンスヘッダーもボディも送らず
  • ただTCP接続を静かに切断するコードです。
  • クライアント側では「接続が切れた?」程度しか見えません。

これを利用して、「100%奇妙なURL」応答もせず接続を切断できます。

メリットはシンプルです。

  • アプリに到達しません。 (フレームワークCPU 0)
  • アクセスログもオプションで完全に残さないようにできます。
  • 「奇妙なURL」はnginx一層で全て除外できるので、アプリ側のログが非常にクリーンになります。

3. blackhole.conf一つで管理するパターン



ルールを毎回サーバーブロックごとに書くより、 私は通常blackhole.confというファイルを一つ置き、共通パターンをここに集めます。

例:

# /etc/nginx/blackhole.conf (パスは自由に)

# === 1. 隠し/構成管理ディレクトリ (.git, .env, IDE など) ===
location ~* (^|/)\.(git|hg|svn|bzr|DS_Store|idea|vscode)(/|$)   { return 444; }
location ~* (^|/)\.env(\.|$)                                    { return 444; }
location ~* (^|/)\.(?:bash_history|ssh|aws|npm|yarn)(/|$)       { return 444; }

# === 2. 使っていない CMS 管理/脆弱性スキャン ===
location ^~ /administrator/                                    { return 444; }  # Joomla など
location ^~ /phpmyadmin/                                      { return 444; }  # 使っていないなら確実にブロック

# === 3. PHP/CGI/WordPress の痕跡 ===
# PHPを全く使わないスタック(例: pure Node, Go, Python)でのみ推奨
location ~* \.(?:php\d*|phtml|phps|phar)(/|$)                  { return 444; }
location ^~ /cgi-bin/                                         { return 444; }
location ~* ^/(wp-admin|wp-includes|wp-content)(/|$)          { return 444; }

# === 4. スキャナが頻繁に探すファイル/パス ===
location ~* ^/(?:info|phpinfo|test|wso|shell|gecko|moon|root|manager|system_log)\.php(?:/|$)? {
    return 444;
}
location ~* ^/(?:autoload_classmap|composer\.(?:json|lock)|package\.json|yarn\.lock|vendor)(?:/|$) {
    return 444;
}
location ~* ^/(?:_profiler|xmrlpc|xmlrpc|phpversion)\.php(?:/|$)? {
    return 444;
}

# === 5. バックアップ/一時/ダンプファイル ===
location ~* \.(?:bak|backup|old|orig|save|swp|swo|tmp|sql(?:\.gz)?|tgz|zip|rar)$ {
    return 444;
}

# === 6. well-known: ACME だけ許可し、残りはカット ===
location ^~ /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
}
location ^~ /.well-known/ {
    return 444;
}

# === 7. メソッドガード(オプション): ほとんど使わないメソッドは事前ブロック ===
# TRACE, CONNECT, WebDAV 系メソッドを使わないなら事前にブロック
if ($request_method !~ ^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$) {
    return 405;
}

これを作っておけば、各サーバーブロックで一行だけ追加します。

http {
    # ...

    server {
        listen 80;
        server_name example.com;

        include /etc/nginx/blackhole.conf;

        # 残りの正常ルーティング設定...
    }
}

これでほとんどの「使っていないパスに対する脆弱性スキャン」は アプリに到達する前にnginxで444で整理されます。


4. ログノイズまで減らしたい場合(オプション)

ここまでで十分爽快ですが、さらにこのブラックホールに落ちるリクエストをアクセスログにも残さないようにできます。

例えば、以下のようにmapを使えます。

http {
    # ブラックホールに送るパターンはログを減らしたい
    map $request_uri $drop_noise_log {
        default                            0;
        ~*^/(?:\.git|\.env)                1;
        ~*^/(wp-admin|wp-includes|cgi-bin) 1;
        ~*\.php(?:/|$)                     1;
    }

    server {
        listen 80;
        server_name example.com;

        include /etc/nginx/blackhole.conf;

        # drop_noise_log が 0 のときだけアクセスログを記録
        access_log /var/log/nginx/access.log combined if=$drop_noise_log;
    }
}
  • $drop_noise_log が 1 のリクエストはログを残しません。
  • 代わりに本当のユーザーリクエスト、正常ルートのログだけがきれいに残ります。

実運用では、最初は一時的にログをオンにしてパターンを検証し、 「正規ユーザーがここに来ることはない」と確信したら if=$drop_noise_log に切り替えることを推奨します。


5. 誤検知(正常リクエストをブロックするミス)を防ぐガイド

この種のブロックルールで最も重要なのは「過度に攻撃的にブロックしない」ことです。いくつかの基準を設けておくと良いでしょう。

  1. スタックに合ったルールだけを有効にする * PHPを全く使わないプロジェクトなら.php全体をブロックするのは便利です。 * 逆にLaravelやWordPressを実際に使っているなら.phpブロックルールは除外すべきです。 * 同様に/cgi-binを実際に使うレガシーシステムならそのルールを除外。

  2. 最初は404/403で始めても良い * すぐに444で切断しても構いませんが、 * 初期はreturn 403;程度で応答を残し、 * 「正規ユーザーがこのURLを入力することがあるか」数日監視した後で444に変更してもOKです。

  3. 危険なパスは保守的に * .git.env、バックアップファイル、phpinfo.phpなどは「誤って外部に露出してはならない」ものです。 * これらは大胆に444でブロックしても運用に影響はほぼありません。

  4. 位置と優先順位 * nginx にはlocationのマッチング優先順位(正確、プレフィックス、正規表現など)が存在します。 * 一般的なアプリケーションルーティングより上に置かないで、通常はinclude blackhole.confをサーバーの先頭に置き、 後にlocation / { ... }などメインハンドラーを置けば問題ありません。


6. アプリケーション、WAF、nginx:それぞれ得意な場所でブロック

この記事で紹介した手法は、あくまで「一次的ノイズ除去」に近いです。

  • nginx ブラックホール:
  • 「意味のない、存在してはならないパス」を初期に切り捨てる役割
  • アプリケーションレベル検証:
  • ドメインロジック、権限、入力値検証などサービス固有のセキュリティ
  • 別途WAF/セキュリティソリューション:
  • パターンベース/シグネチャベース攻撃、L7 DDoS、高度なボットフィルタリング

各層は相互に代替するのではなく、補完し合うと考えると良いです。

その中でnginxブラックホールは:

  • 実装が比較的簡単
  • 効果は意外と大きい
  • 「精神的健康にも良い」副作用もある
  • 導入コスト対効果が非常に高い小さなトリック

まとめ

インターネットに公開されるサービスなら、「奇妙なURLスキャン」を防ぐことはほぼ不可能です。

そこで視点を変えてみましょう。

「やっぱり来るなら、アプリまで持ち込まずに nginx前で静かに整理してしまおう」

blackhole.conf一つで共通パターンを管理し、444応答でアプリ前で不要トラフィックを整理すれば:

  • アプリログはずっとクリーンになります。
  • 監視/分析も楽になります。
  • サーバーリソースも少し余裕が生まれます。

何より、深夜2時にログを開いたときに 無限に続く/wp-login.php行列の代わりに 「本当のユーザー」リクエストだけが見える体験はかなり楽しいです。

運用中のnginxがあれば、今回の機会に 自分だけのblackhole.confを作って導入してみてください。 数日後には「これ、なぜ今までやってなかったのか?」と感じるでしょう。

image of nginx block malicious url