bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. iptables + ipsetによるwp-login.php自動ブロックの仕組み - ① 動作原理と遮断スクリプト

iptables + ipsetによるwp-login.php自動ブロックの仕組み - ① 動作原理と遮断スクリプト

Feb 22, 2026 WordPress bucket-sort

以前にも書きましたが、WordPress サイトを運用していると wp-login.php へ大量の攻撃アクセスが飛んできて、かなりうんざりします。

特に、ロードバランサや AWS WAF を前段に置いていない、小規模な EC2 インスタンスを直接インターネットに公開している構成の場合、マネージドな防御手段をすぐに適用することができません。

かといって、wp-login.php への POST リクエストを延々と投げ続けてくるようなブルートフォース攻撃を放置しておくのも、ログの汚染やリソース消費の観点から好ましくありません。

そこで今回は、執拗に wp-login.php へのアクセスを繰り返すアクセス元を自動的にブラックリスト化する仕組みを、iptables と ipset を使って自作してみます。

全体構成(ipset方式)

今回の仕組みは、以下のように役割分担を行います。

  • Apacheログ解析 → 攻撃元IPの検出
  • ipset → IPアドレスを「期限付き」で保持
  • iptables → ipset に入っているIPを一括で遮断(ルールは1行だけ)

全体の流れは次の通りです。

  1. Apache の access log から、過去30分間の POST /wp-login.php アクセスを抽出する
  2. 抽出したアクセスを送信元IPごとに回数集計する
  3. 設定した閾値を超えたIPアドレスを攻撃元として抽出する
  4. 抽出したIPアドレスを ipset のブラックリストへ追加する(timeout付き)
  5. 指定した時間が経過すると、ipset の timeout により自動的にブロックが解除される

重要なのは、解除処理を一切書かなくてよい点です。

ipset の timeout 機能を利用することで、指定した時間が経過すると自動的にブラックリストから除外されます。

攻撃アクセスを時限的にブロック解除する必要があるかどうかは議論の分かれるところだと思いますが、一定時間を経過したら「許してやる」という意味合いでなく、ブラックリストが増殖し続けることを避けるために時限解除は入れておくべきだと考えました。

スクリプト作成

スクリプトは以下のパスに作成します。

/path/to/block/wp-login-blocker.sh

スクリプト全文

#!/bin/bash
set -eu

# 英語月名(Decなど)を確実に解釈できるように
export LC_ALL=C
# Apacheログが +0900 前提なので JST に固定(mktimeがローカルTZを使うため)
export TZ=Asia/Tokyo

#####################################
# 引数(動作モード)
#####################################
NO_BLOCK=0
case "${1:-}" in
  "" ) ;;
  --no-block|--dry-run ) NO_BLOCK=1 ;;
  -h|--help )
    echo "Usage: $0 [--no-block|--dry-run]"
    exit 0
    ;;
  * )
    echo "Unknown option: $1" >&2
    echo "Usage: $0 [--no-block|--dry-run]" >&2
    exit 2
    ;;
esac

#####################################
# 設定
#####################################
WWW_ROOT="/path/to/www-root"
LOG_REL="logs/access.log"

# 対象 WordPress サイト名(スペース区切り)
TARGET_SITES="sample-site"

WINDOW_MINUTES=30   # 現時点から過去何分間のログを集計するか
THRESHOLD=300       # 何回以上のアクセスをブロック対象にするか

IPSET_NAME="wplogin"
IPSET_TIMEOUT=86400   # 24時間

WORK_DIR="/path/to/block/tmp"
LOCK_FILE="$WORK_DIR/wp-login-blocker.lock"

MAIL_TO="security-team@example.com"
MAIL_SUBJECT="[WP Login Block Alert]"

MAIL_FROM="wp-login-blocker@example.com"
SENDMAIL_BIN="${SENDMAIL_BIN:-/path/to/sendmail}"

#####################################
# 初期化
#####################################
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0

now_epoch=$(date +%s)
cutoff=$((now_epoch - WINDOW_MINUTES * 60))

# 中間ファイル
tmp_last="$WORK_DIR/last-span.log"
tmp_wplogin="$WORK_DIR/last-wp-login.log"

# 以降の処理用
tmp_hits="$WORK_DIR/hits.raw"
tmp_result="$WORK_DIR/hits.tsv"
tmp_ips="$WORK_DIR/ips.txt"

: > "$tmp_last"
: > "$tmp_wplogin"
: > "$tmp_hits"
: > "$tmp_result"
: > "$tmp_ips"

#####################################
# ipset 存在確認(ブロックモードのみ)
#####################################
if [ "$NO_BLOCK" -eq 0 ]; then
  if ! ipset list "$IPSET_NAME" >/dev/null 2>&1; then
      ipset create "$IPSET_NAME" hash:ip timeout "$IPSET_TIMEOUT"
  fi
fi

#####################################
# 1) s-access.log → 過去30分だけ抽出
#####################################
# 注: date外部呼び出しをやめて高速化したまま(mktime利用)
for site in $TARGET_SITES; do
  log="$WWW_ROOT/$site/$LOG_REL"
  [ -f "$log" ] || continue

  gawk -v cutoff="$cutoff" '
  BEGIN {
    mon["Jan"]=1; mon["Feb"]=2; mon["Mar"]=3; mon["Apr"]=4; mon["May"]=5; mon["Jun"]=6;
    mon["Jul"]=7; mon["Aug"]=8; mon["Sep"]=9; mon["Oct"]=10; mon["Nov"]=11; mon["Dec"]=12;
  }
  {
    # Apache形式: [15/Dec/2025:00:08:38 +0900]
    ts = $4
    gsub(/^\[/, "", ts)             # 15/Dec/2025:00:08:38

    split(ts, a, "/")               # a[1]=15, a[2]=Dec, a[3]=2025:00:08:38
    day = a[1]
    month = mon[a[2]]

    split(a[3], b, ":")             # b[1]=2025, b[2]=00, b[3]=08, b[4]=38
    year=b[1]; hh=b[2]; mm=b[3]; ss=b[4]

    epoch = mktime(year " " month " " day " " hh " " mm " " ss)
    if (epoch < cutoff) next

    print $0
  }' "$log" >> "$tmp_last"
done

#####################################
# 2) last-30minutes.log → POST /wp-login.php のみ抽出
#####################################
gawk '
{
  method = $6
  gsub(/"/, "", method)
  if (method != "POST") next

  path = $7

  # 先頭の // を / に正規化(//// でもOK)
  gsub(/^\/+/, "/", path)

  # クエリを除去(?以降)
  sub(/\?.*$/, "", path)

  if (path != "/wp-login.php") next

  print $0
}' "$tmp_last" > "$tmp_wplogin"

#####################################
# 3) last-wp-login.php.log → hits.raw (IP<TAB>site)
#####################################
# 今は site 固定(TARGET_SITES="example" 前提)
SITE_NAME="$TARGET_SITES"
gawk -v site="$SITE_NAME" '{ print $1 "\t" site }' "$tmp_wplogin" > "$tmp_hits"

#####################################
# 集計 & 閾値判定(Site をユニーク化)
#####################################
awk '
{
    ip = $1
    s  = $2
    count[ip]++

    key = ip SUBSEP s
    if (!seen[key]++) {
        sites[ip] = (sites[ip] ? sites[ip] "," s : s)
    }
}
END {
    for (ip in count) {
        if (count[ip] >= '"$THRESHOLD"') {
            print ip "\t" count[ip] "\t" sites[ip]
        }
    }
}
' "$tmp_hits" > "$tmp_result"

cut -f1 "$tmp_result" > "$tmp_ips"

#####################################
# メール送信(本文に確実に載せる)
#####################################
send_text_mail() {
  local subject="$1"
  local to="$2"

  if [ -x "$SENDMAIL_BIN" ]; then
    {
      echo "From: ${MAIL_FROM}"
      echo "To: ${to}"
      echo "Subject: ${subject}"
      echo "MIME-Version: 1.0"
      echo "Content-Type: text/plain; charset=UTF-8"
      echo "Content-Transfer-Encoding: 8bit"
      echo
      cat
    } | "$SENDMAIL_BIN" -t
  else
    mail -s "$subject" "$to"
  fi
}

#####################################
# 通知(常に実施)
#####################################
if [ -s "$tmp_ips" ]; then
    mode_text=$([ "$NO_BLOCK" -eq 1 ]
        && echo "【検知のみ:ブロックは実行していません】" || echo "【ブロック実施:ipset に追加しました】")
    {
      echo "${mode_text}"
      echo
      echo "条件: 過去 ${WINDOW_MINUTES} 分間の POST /wp-login.php が ${THRESHOLD} 回以上"
      if [ "$NO_BLOCK" -eq 0 ]; then
        echo "ブロック時間: ${IPSET_TIMEOUT} 秒(ipset: ${IPSET_NAME})"
      fi
      echo
      echo "IP              Count   Site"
      echo "----------------------------------------"
      sed $'s/\t/    /g' "$tmp_result"
      echo
      echo "---- debug files ----"
      echo "last span log: $tmp_last"
      echo "wp-login only: $tmp_wplogin"
      echo
    } | send_text_mail "$MAIL_SUBJECT" "$MAIL_TO"
fi

#####################################
# ブロック(ブロックモードのみ)
#####################################
if [ "$NO_BLOCK" -eq 0 ] && [ -s "$tmp_ips" ]; then
    while read -r ip; do
        ipset add "$IPSET_NAME" "$ip" timeout "$IPSET_TIMEOUT" 2>/dev/null || true
    done < "$tmp_ips"
fi

今回は、仕組みの全体像とスクリプトのみを紹介しました。

次回は、初期セットアップの手順や実際の実行方法、運用上の注意点について説明します。

Linux セキュリティ WordPress Iptables Ipset
← iptablesとipsetによるLinuxサーバーの基本的なファイアウォール設定 iptables + ipsetによるwp-login.php自動ブロックの仕組み - ② 初期セットアップと自動実行設定 →

Related Posts

  • iptables + ipsetによるwp-login.php自動ブロックの仕組み - ③ 運用結果と安全なリセット手順 Feb 24, 2026
  • iptables + ipsetによるwp-login.php自動ブロックの仕組み - ② 初期セットアップと自動実行設定 Feb 23, 2026
  • iptablesとipsetによるLinuxサーバーの基本的なファイアウォール設定 Feb 21, 2026
  • サービスを実行するユーザにログインを許可した場合のセキュリティリスクを考える Feb 18, 2026

Table of Contents

  • 全体構成(ipset方式)
  • スクリプト作成

Recent Posts

  • Laravel の Event / Listener で Pub/Sub を実装する Apr 2, 2026
  • [C#] delegate と event の仕組みを整理する Apr 1, 2026
  • Pub/Sub パターンとは何か Mar 31, 2026
  • PHP/Laravel で値の状態を判定するヘルパ関数まとめ Mar 30, 2026
  • Filament の dehydrated メソッドとは何か Mar 29, 2026

Categories

  • AWS27
  • C#22
  • .NET20
  • Laravel16
  • Linux12
  • Apache8
  • MySQL8
  • PHP8
  • DynamoDB6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • セキュリティ3
  • .NET Framework1
  • Aurora1
  • Filament1
  • Git1
  • SQS1

Tags

  • AWS
  • C#
  • .NET
  • Laravel
  • PHP
  • MySQL
  • セキュリティ
  • Linux
  • Apache
  • Code Snippet
  • DynamoDB
  • NoSQL
  • PHP-FPM
  • RDS
  • DoS
  • Nginx
  • Windows
  • WordPress
  • パフォーマンス
  • 監視
  • Amazon Linux 2023
  • CMS
  • Docker
  • Ipset
  • Iptables
  • OPCache
  • Webサーバー
  • 認可
  • Aurora
  • Blade
Powered by Hugo & Explore Theme.