1. 実際の現場:UFWとFail2banを有効化していたのにサーバーが侵害された事例
Ubuntuサーバーを管理する多くのエンジニアは、UFWで不要なポートを閉じ、Fail2banを導入してSSHのブルートフォース攻撃を防いだら、それでセキュリティ対策は万全だと安心しがちです。しかし、実際の運用現場はそれほど甘くありません。
先日、私たちが運用しているWebアプリケーションで、usersテーブルの全データが突如ダンプ(漏洩)されるインシデントが発生しました。ログを確認したところ、UFWは正常に稼働しており、Fail2banも一切検知していませんでした。攻撃者は検知しきい値を超えるような数千回ものブルートフォース攻撃を仕掛けたわけではありません。SQL Injectionの脆弱性を突くペイロードを含んだHTTPリクエストを、わずか3回送信しただけだったのです。
' UNION SELECT id, username, password_hash FROM admin_users--
Nginxは通常の正規アクセスと同様にポート443経由でパケットを受信し、バックエンドのPHP-FPMが正常に処理してHTTP 200ステータスコードとともに管理者クレデンシャルをすべて返してしまっていました。
従来のネットワークファイアウォールは、HTTPパケットの内部データに対して完全に「盲目」です。正常なトラフィックも悪意あるペイロードも、同一のパケットとして素通りさせてしまいます。
2. アプリケーション層(Layer 7)に潜む致命的なセキュリティホール
的確に防御するためには、まず各ツールの守備範囲を正しく理解する必要があります。
- UFW / Iptables / NFTables: Layer 3(ネットワーク層)およびLayer 4(トランスポート層)を防御します。送信元/送信先IP、ポート番号、TCP/UDPフラグを検査しますが、HTTPボディの内容を読み取ったり復号したりすることは一切できません。
- Fail2ban: 本質的には事後対応型(リアクティブ)のツールです。ログをスキャンして異常な振る舞いを検知し、IPをブロックします。攻撃者が検知されないよう時間をかけてじわじわ攻める「Low and Slow」攻撃を仕掛けてきた場合、Fail2banはほぼ無力です。
実際の脅威は、常にデータ部分に潜んでいます。URLのクエリパラメータ、カスタムヘッダー、POSTボディに埋め込まれたJSONペイロードなどです。SQL Injection(SQLi)、Cross-Site Scripting(XSS)、Remote Code Execution(RCE)を確実に検知・遮断するには、バックエンドに渡す前にデータの1バイト単位まで精査するチェックポイントが必要です。それこそがWeb Application Firewall(WAF)の役割です。
3. 代表的な3つのWAFソリューションの比較
この課題に対し、DevOpsエンジニアは通常3つの選択肢を検討します。
- Cloud WAFの導入(Cloudflare、AWS WAFなど): DNSの切り替えだけで迅速に導入でき、広範なCDNネットワークを利用できます。しかし、リクエスト数に応じてコストが跳ね上がる点や、機密データがサードパーティのインフラを通過する点がデメリットです。また、無料プランや安価なプランではルールのカスタマイズが厳しく制限されます。
- Nginx設定ファイル内で自作Regexによるブロック: たとえば
if ($query_string ~* "union.*select") { return 403; }のようなルールを書く方法です。しかし、この方法は簡単に回避されます。攻撃者がuNiOn/**/sElEcTのように記述したり、%75%6e%69%6f%6eとURLエンコードするだけで簡単にすり抜けられてしまいます。さらに、複雑な正規表現を多用すると、トラフィック急増時にNginxのCPU使用率が15〜30%も跳ね上がる原因になります。 - ModSecurity + OWASP CRSによるオンプレミスWAFの構築: ModSecurity v3エンジン(libmodsecurity)と標準ルールセットであるOWASP Core Rule Set(CRS)を組み合わせるアプローチです。ライセンス費用はゼロ。Nginxプロセス内で直接動作するため、データを100%自社で管理でき、各APIの仕様に合わせた自由なチューニングが可能です。
4. UbuntuにおけるModSecurity v3とOWASP CRSの構築手順
バージョン3以降、ModSecurityは処理エンジン(libmodsecurity)とWebサーバーのコネクターを完全に分離しました。この動的モジュールアーキテクチャにより、以前の肥大化していたv2と比べてNginxがはるかに安定して動作するようになっています。
以下の手順は、Nginxが稼働しているUbuntu 22.04 LTS環境を対象としています。
ステップ1:必要なビルドツールのインストール
パッケージリポジトリを更新し、ソースコードのビルドに必要なパッケージをインストールします。
sudo apt update
sudo apt install -y build-essential libpcre3 libpcre3-dev zlib1g zlib1g-dev \
libssl-dev libxml2-dev libxslt1-dev libgeoip-dev liblmdb-dev libyajl-dev \
git automake libtool curl libfuzzy-dev
ステップ2:ModSecurity v3コアエンジンのコンパイル
libmodsecurityの公式ソースコードをクローンし、コンパイルを実行します。
cd /usr/local/src
sudo git clone --depth 1 -b v3/master https://github.com/owasp-modsecurity/ModSecurity.git
cd ModSecurity
sudo git submodule init
sudo git submodule update
sudo ./build.sh
sudo ./configure
sudo make -j$(nproc)
sudo make install
コンパイルにはCPUコア数に応じて約3〜8分かかります。正常に完了すると、コアライブラリは /usr/local/modsecurity/ にインストールされます。
ステップ3:Nginxのバージョンに合わせたコネクターモジュールのコンパイル
Nginxの動的モジュールは、現在稼働しているバイナリと完全に一致するバージョンのソースコードからビルドする必要があります。まずは現在のバージョンを確認します。
nginx -v
ターミナルに出力されたバージョン(例:nginx version: nginx/1.24.0)を確認し、該当バージョンのソースtarballのダウンロードとModSecurity-nginxコネクターのクローンを行います。
cd /usr/local/src
sudo git clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar -xzvf nginx-1.24.0.tar.gz
cd nginx-1.24.0
# 稼働中のバイナリと互換性のあるモジュールをコンパイル
./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
make modules
# コンパイルされたモジュールバイナリをNginxモジュールディレクトリにコピー
sudo cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules/
ステップ4:メイン設定ファイルへのモジュールの読み込み
設定ファイル /etc/nginx/nginx.conf を開きます。
sudo nano /etc/nginx/nginx.conf
ファイルの先頭(events {} ブロックよりも前)に以下のディレクティブを追加します。
load_module modules/ngx_http_modsecurity_module.so;
ステップ5:OWASP CRSルールセットのダウンロードと設定
ModSecurityエンジンが「銃身」だとすれば、OWASP CRSはその中に入る「弾薬」に相当します。作業用ディレクトリを作成し、ルールセットをダウンロードします。
sudo mkdir -p /etc/nginx/modsec
cd /usr/local/src/ModSecurity
sudo cp modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf
sudo cp unicode.mapping /etc/nginx/modsec/
# 安定性に優れたOWASP CRS v3.3.5をダウンロード
cd /etc/nginx/modsec
sudo git clone -b v3.3.5 https://github.com/coreruleset/coreruleset.git owasp-crs
sudo cp owasp-crs/crs-setup.conf.example owasp-crs/crs-setup.conf
Nginxからまとめて読み込むための統合設定ファイル /etc/nginx/modsec/main.conf を作成します。
sudo nano /etc/nginx/modsec/main.conf
以下の3行の設定を記述します。
Include /etc/nginx/modsec/modsecurity.conf
Include /etc/nginx/modsec/owasp-crs/crs-setup.conf
Include /etc/nginx/modsec/owasp-crs/rules/*.conf
ステップ6:WAFのブロックモード有効化
デフォルト状態のModSecurityは DetectionOnly モード(ログ記録のみでリクエストは遮断しない)で動作します。実際に攻撃をブロックするために、/etc/nginx/modsec/modsecurity.conf でブロックモードを有効化します。
sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/nginx/modsec/modsecurity.conf
あわせて、監視用のアクティビティを記録する監査ログ(Audit Log)の出力先を設定します。
SecAuditLog /var/log/nginx/modsec_audit.log
ステップ7:Virtual HostでのModSecurityの有効化
サーバーブロック設定ファイル(例:/etc/nginx/sites-available/default)で、WAFによる保護を適用するサイトを指定します。
server {
listen 80;
server_name example.com;
# このVirtual Host全体でWAFを有効化
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
location / {
try_files $uri $uri/ =404;
}
}
設定ファイルの構文チェックを行い、Nginxをリロードします。
sudo nginx -t && sudo systemctl reload nginx
ステップ8:テストペイロードの送信とログ確認
curlコマンドを使用して、典型的なXSSペイロードを含むテストリクエストを送信します。
curl -I "http://localhost/?q=<script>alert(1)</script>"
HTTP 200ではなく、Nginxは即座に以下のように応答します。
HTTP/1.1 403 Forbidden
Server: nginx/1.24.0
Content-Type: text/html
Connection: keep-alive
監査ログファイルを開いて、遮断理由の詳細を確認します。
sudo tail -n 30 /var/log/nginx/modsec_audit.log
ログにはRule ID 941100(XSS Filter – Category 1)がトリガーされたことが明記されます。リクエストの合計アノマリスコア(Anomaly Score)がデフォルトのしきい値(5点)を超えたため、ModSecurityがその場で接続を遮断したことが確認できます。
5. 実践ノウハウ:アプリを壊さずに誤検知(False Positive)に対処する方法
WAFの運用で最も懸念されるのが、正当なユーザーアクセスを誤って遮断してしまう「誤検知(False Positive)」です。HTMLタグを含むリッチテキストエディタのフォーム送信や、ネストされたJSONペイロードを送信するAPIなどは、ルール 942100(SQLi)によって誤検知されやすい典型例です。
サービス停止トラブルを防ぐため、以下の2段階アプローチを適用することをお勧めします。
- 3〜7日間のログ監視運用を実施する: まずは
SecRuleEngine DetectionOnlyに設定します。毎日ログを確認し、正規ユーザーのリクエストが誤って検知されていないかを精査します。 - エンドポイント単位でホワイトリスト化する: サイト全体でルールを完全に無効化するのは避けてください。たとえば
/api/v1/posts/editorエンドポイントでルール942100が誤検知される場合は、/etc/nginx/modsec/main.confの末尾に除外設定を追加します。
# 記事エディタのエンドポイント専用にルール942100をホワイトリスト化
SecRule REQUEST_URI "@beginsWith /api/v1/posts/editor" \
"id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"
この方法を採用することで、データベースへの侵入経路を厳重に防御しつつ、アプリケーションの業務機能を問題なく円滑に稼働させることができます。

