搭建自动封禁 WordPress 登录攻击的 Matomo + Apache 脚本

在管理 WordPress 网站时,wp-login.php 常常成为攻击目标。本文分享我如何利用 Matomo API 获取访问数据,并自动生成 Apache 封禁规则,实现 T+1 自动封禁恶意 IP。


1. 准备工作

1.1 获取 Matomo 访问 Token

1. 登录 Matomo 后台。

2. 打开 个人 → 设置 → 安全 生成验证令牌

3. 保存 token,用于脚本调用 API。

1.2 确认 Apache 配置可用

  • Apache 版本 ≥ 2.4。
  • 确认 /etc/apache2/apache2.conf 中包含:
IncludeOptional conf-enabled/*.conf
IncludeOptional sites-enabled/*.conf
  • 确保能通过 apachectl graceful 重载配置。

2. Python 脚本实现

2.1 核心功能

  • 调用 Matomo API 获取最近访问 wp-login.php 的 IP。
  • 检查 IP 是否在白名单。
  • 历史封禁 IP 累加,不覆盖。
  • 打印日志至指定路径,方便记录。

2.2 完整脚本 

matomo_block.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import requests
import subprocess
import ipaddress
import os
import traceback
from datetime import datetime

# ================= 配置 =================
MATOMO_API = "http://matomo.ruianding.com/index.php"
SITE_ID = 1
TOKEN_AUTH = "{your_matomo_api_token}"
BLOCK_CONF = "/etc/apache2/block_wp.conf"  # Apache 全局封禁配置
LIMIT = 1000  # 拉取最近访问数
LOG_FILE = "matomo_block.log"
HISTORY_FILE = "blocked_ips.txt"  # 历史封禁 IP 文件

# 白名单 IP / 网段 (CIDR 支持)
WHITELIST = [
    "{白名单 IPV4 地址}"
]

# ================= 函数 =================
def log(msg):
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{timestamp}] {msg}"
    print(line)
    with open(LOG_FILE, "a") as f:
        f.write(line + "\n")

def ip_in_whitelist(ip_str):
    """判断 ip 是否在白名单"""
    ip = ipaddress.ip_address(ip_str)
    for net in WHITELIST:
        try:
            if '/' in net:
                if ip in ipaddress.ip_network(net):
                    return True
            else:
                if ip == ipaddress.ip_address(net):
                    return True
        except ValueError:
            continue
    return False

def load_blocked_ips():
    """读取历史封禁 IP"""
    blocked = set()
    if os.path.exists(HISTORY_FILE):
        with open(HISTORY_FILE) as f:
            for line in f:
                line = line.strip()
                if line:
                    blocked.add(line)
    return blocked

def save_blocked_ips(blocked_ips):
    """保存累积封禁 IP"""
    with open(HISTORY_FILE, "w") as f:
        for ip in sorted(blocked_ips):
            f.write(ip + "\n")

def get_wp_login_ips(limit=LIMIT):
    """从 Matomo API 获取访问 wp-login.php 的 IP"""
    params = {
        "module": "API",
        "method": "Live.getLastVisitsDetails",
        "idSite": SITE_ID,
        "period": "day",
        "date": "today",
        "format": "JSON",
        "filter_limit": limit,
        "token_auth": TOKEN_AUTH
    }
    r = requests.post(MATOMO_API, data=params)
    r.raise_for_status()
    data = r.json()

    wp_ips = set()
    for visit in data:
        for action in visit.get("actionDetails") or []:
            url = action.get("url")
            if url and "wp-login.php" in url:
                ip = visit.get("visitIp")
                if ip:
                    wp_ips.add(ip)
    return wp_ips

def write_apache_conf(blocked_ips):
    """生成 Apache 全局封禁规则,使用 <If> 判断 REMOTE_ADDR"""
    lines = ["# Auto-generated by matomo_block.py"]
    blocked_count = 0
    for ip in sorted(blocked_ips):
        if ip_in_whitelist(ip):
            continue
        lines.append(f'<If "%{{REMOTE_ADDR}} == \'{ip}\'">')
        lines.append("    Require all denied")
        lines.append("</If>")
        blocked_count += 1

    with open(BLOCK_CONF, "w") as f:
        f.write("\n".join(lines))
    log(f"[+] Wrote {blocked_count} rules to {BLOCK_CONF}")

def reload_apache():
    """重载 Apache"""
    try:
        subprocess.run(["apachectl", "graceful"], check=True)
        log("[+] Apache reloaded")
    except subprocess.CalledProcessError as e:
        log(f"[-] Apache reload failed: {e}")
        log(traceback.format_exc())

# ================= 主程序 =================
if __name__ == "__main__":
    try:
        old_blocked = load_blocked_ips()
        log(f"[+] Loaded {len(old_blocked)} previously blocked IPs")
        new_ips = get_wp_login_ips()
        log(f"[+] Fetched {len(new_ips)} IPs accessing wp-login.php today")

        # 过滤白名单并累加
        to_block = {ip for ip in new_ips if not ip_in_whitelist(ip)}
        all_blocked = old_blocked | to_block
        save_blocked_ips(all_blocked)
        log(f"[+] Total {len(all_blocked)} IPs will be blocked (including history)")

        write_apache_conf(all_blocked)
        reload_apache()
    except Exception as e:
        log(f"[-] Error: {e}")
        log(traceback.format_exc())

3. Apache 配置说明

  • 封禁规则通过 <RequireAll> 包裹,避免负向 Require 报错。
  • 白名单可支持单个 IP 或 CIDR 网段。
  • 历史封禁 IP 会累加,不会覆盖。


3.1 Apache 加载自定义封禁规则


为了让 Apache 实际应用我们生成的 /etc/apache2/block_wp.conf,需要在主配置里 Include 它:

# Include custom block rules for wp-login.php
Include /etc/apache2/block_wp.conf
  • 你可以加在 /etc/apache2/apache2.conf 或对应的虚拟主机配置文件中。
  • 这样脚本生成的新规则每次重载 Apache 都会生效。
  • 注意 Include 路径必须正确,否则 Apache 启动会报错。

4. 调试与排错

4.1 Apache 无法识别配置报错 – Require not ip

使用 <RequireAll> 包裹负向规则即可。

未被包裹的配置信息(Apache 无法识别)
包裹后的封禁配置

4.2 ServerName 相关报错

AH00558: apache2: Could not reliably determine the server's fully qualified domain name

解决方式:为Apache配置默认域名。

# /etc/apache2/conf-available/servername.conf
ServerName www.ruianding.com
sudo a2enconf servername
sudo systemctl reload apache2

不会影响其他子域名,例如 grafana.ruianding.com。

4.3 Cron 执行配置

  • 确保 Python 路径正确。
  • 例子:每天 0 点自动执行:
  • 由于脚本已自带日志打印功能,所以无需输出Cron日志
0 0 * * * /usr/bin/python3 /data/matomo/matomo_block.py

5. 进一步优化

5.1 全局封禁 vs 单路径封禁的调整

在最初的实现中,我的 Matomo IP 封禁脚本是针对单个路径 /wp-login.php 的,使用 Apache <RequireAll>/<RequireAny> 配置来拒绝访问。但我想到一个问题:

无法覆盖攻击者的其他尝试路径 攻击者可能通过不同的 URL 尝试访问登录页,例如 www.ruianding.com/blog/login-wp.php。如果只封单路径,攻击者仍然可以通过其他路径尝试暴力登录。

因此,我做了两项关键调整:

全局封禁 使用 Apache <If> 判断 REMOTE_ADDR:

<If "%{REMOTE_ADDR} == '1.2.3.4'">
    Require all denied
</If>

这样可以针对每个攻击 IP 全局拒绝访问,不再依赖路径限定,攻击者无论访问哪个 URL 都会被阻止。


6. 总结

通过 Matomo + Python + Apache 自动化封禁,减少了手动排查攻击 IP 的时间,提高了网站安全性。

整个流程包括:

  1. Matomo API 调用调试
  2. Python 自动化脚本生成封禁规则
  3. Apache 配置更新与重载
  4. Cron 自动化执行
  5. 调试、排错与优化