| 元記事URL | https://qiita.com/jiis-sasaki/items/7affa2e2ef22ebf93fa6 |
|---|---|
| はてなブックマーク | https://b.hatena.ne.jp/ao41/20260913#bookmark-4792912550887628962 |
| ブックマーク日時 | 2026-09-13 23:04:48 JST(2026-09-13T14:04:48Z) |
| はてブ数 | 92 users(記事取得時点:LGTM 33・コメントなし) |
| 著者 | @jiis-sasaki(Qiita)/ 記事投稿日: 2026-09-10 |
| タグ | あとで読む |
tail で眺めても1日数万行から異常だけを拾うことはできません。本記事は SSHでサーバに入れるWeb担当者・サイト運営者・制作会社 向けに、アクセスログを 7本のコピペOKなコマンド に通して「見るべき数行」まで絞り込み、見つけた攻撃元IPを fail2banで自動遮断するまで をまとめた実践ガイドです。前提は nginx / Apache の combined 形式ログ(WordPress特有の見どころにも触れます)。
ここを確認しないと以降の集計は全部むだになります。Cloudflare・ロードバランサ・リバースプロキシ配下では、何もしなければ記録されるのはプロキシのIPです。上位が全部同じIPになり、そのまま fail2ban に食わせると自サイトへの入口ごと遮断してしまいます。
tail -1 /var/log/nginx/access.log
set_real_ip_from 173.245.48.0/20; # 信頼するプロキシのレンジを列挙
real_ip_header CF-Connecting-IP; # 既定は X-Real-IP
real_ip_recursive off;
nginx -V 2>&1 | tr ' ' '\n' | grep realip で確認(パッケージ版には通常含まれる)set_real_ip_from に書いていない相手からのヘッダは無視される。0.0.0.0/0 のように全開にすると誰でもIPを詐称できるので、必ず経路のレンジだけ書くログ書式を %h から %a に変えるのがポイント(%a = 復元後のクライアントIP、%{c}a = 直前の接続元=プロキシ)。
RemoteIPHeader CF-Connecting-IP
RemoteIPTrustedProxy 173.245.48.0/20
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_realip
CustomLog ${APACHE_LOG_DIR}/access.log combined_realip
反映後、自分の回線からサイトを開いて tail -1 に自分のIPが出れば準備完了です。
combined 形式は nginx・Apache ともフィールド位置が同じなので、同じ awk がそのまま使えます。
203.0.113.10 - - [10/Sep/2026:07:12:33 +0900] "POST /wp-login.php HTTP/1.1" 200 1234 "-" "curl/8.5.0"
$1 $2$3 $4 $5 $6 $7 $8 $9 $10 $11 $12
$1 = 送信元IP / $6 = メソッド(先頭に " が付く)/ $7 = パス / $9 = ステータス / $10 = 応答サイズzcat -f は非圧縮ファイルもそのまま流すので .gz と生ログを混ぜて渡せるLOGS="/var/log/nginx/access.log /var/log/nginx/access.log.1 /var/log/nginx/access.log.*.gz"
zcat -f $LOGS | wc -l
Apache は /var/log/apache2/access.log*(RHEL系は /var/log/httpd/access_log*)に読み替え。
zcat -f $LOGS | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
zcat -f $LOGS | awk '$6=="\"POST" && $7 ~ /wp-login\.php|xmlrpc\.php/ {print $1}' \
| sort | uniq -c | sort -rn | head -20
wp-login.php にPOSTが飛ぶのは担当者がログインした回数だけ。1つのIPから数百〜数千回あれば総当たり攻撃(ブルートフォース)xmlrpc.php は使っていなければ本来0件。数字が立つこと自体が異常zcat -f $LOGS | awk '$9==404 {print $1}' | sort | uniq -c | sort -rn | head -20
zcat -f $LOGS | awk '$9==404 {print $7}' | sed 's/?.*//' \
| sort | uniq -c | sort -rn | head -30
/.env, /.git/config, /wp-config.php.bak, /phpinfo.php 等)。このリストは「自分のサイトで絶対に200を返してはいけないパスの一覧」でもあるzcat -f $LOGS | awk '$9 ~ /^2/ && $7 ~ /\.env|\.git|\.bak|\.old|\.sql|\.zip|wp-config|phpinfo|adminer|phpmyadmin|\.DS_Store/ {print $7}' \
| sed 's/?.*//' | sort | uniq -c | sort -rn | head -30
zcat -f $LOGS | awk '$9 ~ /^2/ {b[$1]+=$10} END {for (i in b) printf "%15.0f %s\n", b[i], i}' \
| sort -rn | head -20
%b は0バイト時 - だが、awkの加算では0扱いなので影響なし)zcat -f $LOGS | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn | head -20
- やツール名が上位なら集計1〜3と突き合わせる。ただしUAは自由に名乗れるので単独の根拠にはしない手で叩くのは最初の1回だけにして、あとは日次で回し前日との差分だけを見るのが続けるコツ。スクリプト /usr/local/bin/access-log-digest.sh を cron で毎朝6:05に回し、前日分のダイジェスト差分をメールで受け取る構成が紹介されています。
404を出しているIP上位10 と 2xxで返っている要注意パス(空であること) の2集計を日次出力し diff -u で差分確認集計2・3で常連が特定できたら自動遮断へ。fail2ban の既定値は maxretry=5 / findtime=10m / bantime=10m(1.1.0)=「10分以内に5回で10分BAN」。
nginx-http-auth / nginx-limit-req / nginx-botsearch)はいずれもエラーログを見る。wp-login.php へのPOSTはアクセスログにしか出ないため、フィルタを1つ自作する必要があります。
# /etc/fail2ban/filter.d/wordpress-auth.conf
[Definition]
failregex = ^<HOST> .* "POST [^"]*/(wp-login\.php|xmlrpc\.php)
ignoreregex =
# /etc/fail2ban/jail.local
[wordpress-auth]
enabled = true
filter = wordpress-auth
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 10 # 正規の打ち間違いを弾かない程度に緩める
findtime = 10m
bantime = 1h
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 # 自社オフィス・監視サービスのIPを必ず入れる
[recidive] # 繰り返しBANされる常習者を長期BAN
enabled = true
# 手元のログに何件マッチするか確認(BANはしない)
fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-auth.conf
fail2ban-client reload
fail2ban-client status wordpress-auth # 現在のBAN一覧
fail2ban-client set wordpress-auth unbanip 203.0.113.10 # 誤爆の解除
ignoreip に自社IP・監視サービス・決済/予約システムのWebhook送信元を先に入れる。自分が締め出されるのが最も多い事故maxretry はいきなり絞らない。数日 bantime = 10m で様子を見てから伸ばす/etc/logrotate.d/nginx の rotate を確認し、compress 併用で90日程度まで伸ばすaccess_log ... combined buffer=64k flush=5m; だと直近の行はすぐ出ない。調査中は flush を意識# nginx: ヘルスチェックだけログに書かない
location = /healthz {
access_log off;
return 200;
}
# Apache: 条件付きロギング
SetEnvIf Request_URI "^/healthz$" dontlog
CustomLog ${APACHE_LOG_DIR}/access.log combined_realip env=!dontlog
ignoreip を先に埋め、fail2ban-regex で当たりを確認してから有効化する