HQY

×

Nginx 配置 IP 黑名单与自动封禁

hqy hqy 发表于2026-10-10 14:43:51 浏览124 评论0

抢沙发发表评论

服务器的 Nginx IP 黑名单完整配置,包括手动封禁、自动封禁、滚动 60 秒请求计数、封禁到期、systemd 守护运行、验证与故障排查。

geo 放在 http {} 中,if 判断放在各个 server {} 中;自动封禁程序只修改独立的自动黑名单文件,不覆盖手动黑名单。

统一使用以下路径:

项目
路径或规则
Nginx 主配置
/usr/local/nginx/conf/nginx.conf
手动黑名单
/usr/local/nginx/conf/blacklist/ip_blacklist.conf
自动黑名单
/usr/local/nginx/conf/blacklist/auto_ip_blacklist.conf
自动封禁脚本
/usr/local/nginx/scripts/auto_ip_blacklist.py
访问日志
/usr/local/nginx/logs/access.log
自动封禁规则
同一 IP 在滚动 60 秒内对 /api/ 发起超过 100 次请求
默认封禁时间
1 小时
拦截状态码
HTTP 403

请求处理流程:

  • • 客户端请求进入 Nginx。
  • • Nginx 通过 geo 判断客户端 IP 是否在黑名单中。
  • • 已封禁的 IP 返回 HTTP 404。
  • • 自动封禁程序持续读取 Nginx 访问日志,只统计 /api/ 请求。
  • • 同一 IP 在滚动 60 秒内请求次数达到 101 次时,写入自动黑名单。
  • • Nginx 配置检查通过后重新加载,使新黑名单生效。
  • • 默认封禁 1 小时,到期后自动解除。

注意: 自动封禁统计的是 HTTP 请求次数,不是并发连接数。轮询和移动端高频请求可能导致正常用户超过阈值,部署初期应重点观察误封情况。

创建目录和黑名单文件

1、创建 IP 黑名单文件

mkdir -p /usr/local/nginx/conf/blacklist 
mkdir -p /usr/local/nginx/scripts 

touch /usr/local/nginx/conf/blacklist/ip_blacklist.conf 
touch /usr/local/nginx/conf/blacklist/auto_ip_blacklist.conf

文件说明:

  • • ip_blacklist.conf:手动维护的黑名单。
  • • auto_ip_blacklist.conf:程序自动生成的黑名单。

不要在自动黑名单文件中手动添加需要长期保留的 IP,否则程序刷新文件时可能覆盖这些条目。

2、手动封禁 IP

# 编辑

vim /usr/local/nginx/conf/blacklist/ip_blacklist.conf

# 
例如

192.168.1.10 1;
191.168.1.25 1;

# 
以上为文档示例 IP,不代表真实攻击者地址。

# 
每一行的格式为:

IP地址 1;

如需手动解封,删除对应行并重新加载 Nginx。

配置 Nginx 全局黑名单识别

修改 nginx.conf

在现有的 http {} 内添加以下配置。你之前已经添加过该配置,因此请检查是否存在,避免重复定义。

http {
    # 其他已有配置……

    geo $blacklist_ip {
        default 0;
        include /usr/local/nginx/conf/blacklist/ip_blacklist.conf;
        include /usr/local/nginx/conf/blacklist/auto_ip_blacklist.conf;
    }

    # 其他已有配置……
    
}

geo 必须放在 http {} 作用域中,不能放在 server {} 中。

在站点中拦截黑名单 IP

3、在 server 站点配置中拦截

在需要保护的 server {} 内添加:

server {
    listen 443 ssl;
    server_name demo.com;
    
    # 黑名单 IP 拦截
    if ($blacklist_ip = 1) {
        return 403;
    }

    # 原有 SSL、静态资源、/api 反向代理等配置保持不变
}

if 与左括号之间必须有空格。不要写成 if($blacklist_ip = 1)。

检查配置并重新加载

/usr/local/nginx/sbin/nginx -t

/usr/local/nginx/sbin/nginx -s reload

cat /usr/local/nginx/conf/blacklist/ip_blacklist.conf

确认完整日志格式

如果你的日志使用了自定义 log_format,需要根据实际格式调整,尤其是有 CDN 或反向代理时,要先确保记录的是可信的真实客户端 IP。

grep -nE 'log_format|access_log' /usr/local/nginx/conf/nginx.conf

sed -n '45,60p' /usr/local/nginx/conf/nginx.conf

tail -n 5 /usr/local/nginx/logs/access.log

配置自动封禁程序

程序需要 Python 3。先检查:

python3 --version

如果没有 Python 3,请先安装合适的 Python 3 运行环境,不要直接把下面的脚本交给 CentOS 7 默认的 Python 2.7 执行。

1、创建脚本:

vim /usr/local/nginx/scripts/auto_ip_blacklist.py

2、 脚本内容

#!/usr/bin/env python3

import os
import re
import time
import json
import ipaddress
import subprocess
from collections import defaultdict, deque
from datetime import datetime

LOG_FILE = "/usr/local/nginx/logs/access.log"
BLACKLIST_FILE = (
    "/usr/local/nginx/conf/blacklist/"
    "auto_ip_blacklist.conf"
)
STATE_FILE = (
    "/usr/local/nginx/conf/blacklist/"
    "auto_ip_state.json"
)
NGINX = "/usr/local/nginx/sbin/nginx"

THRESHOLD = 60
WINDOW_SECONDS = 100
BAN_SECONDS = 3600

# 仅统计 /api/ 请求。
# 滚动 60 秒内达到 101 次请求时封禁。
WHITELIST = {
    "127.0.0.1",
    # "替换为你自己的固定公网管理 IP",
}

# 不参与自动封禁请求次数统计的接口路径
# 注意:这里只排除统计,不绕过 Nginx IP 黑名单
EXCLUDED_PATHS = {
    "/api/test",
    # 后续需要排除的接口继续添加,每行一个
}

LOG_RE = re.compile(
    r'^(\S+)\s+-\s+(?:\S*)\s+\[([^\]]+)\]\s+"([^"]*)"'
)

requests = defaultdict(deque)
banned_until = {}


defload_state():
    global banned_until

    try:
        withopen(STATE_FILE, "r") as f:
            data = json.load(f)

        now = time.time()
        banned_until = {
            ip: float(expiry)
            for ip, expiry in data.items()
            iffloat(expiry) > now and ip notin WHITELIST
        }

    except (OSError, ValueError, TypeError):
        banned_until = {}


defsave_state():
    tmp = STATE_FILE + ".tmp"

    withopen(tmp, "w") as f:
        json.dump(banned_until, f)

    os.replace(tmp, STATE_FILE)


defread_blacklist():
    try:
        withopen(BLACKLIST_FILE, "r") as f:
            return f.read()
    except FileNotFoundError:
        return""


defreload_nginx():
    test = subprocess.run(
        [NGINX, "-t"],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=True
    )

    print(test.stdout, flush=True)

    if test.returncode != 0:
        returnFalse

    result = subprocess.run(
        [NGINX, "-s", "reload"],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        universal_newlines=True
    )

    print(result.stdout, flush=True)
    return result.returncode == 0


defsync_blacklist():
    """
    根据当前未过期的封禁记录生成黑名单。
    文件有变化时先备份原内容,检查并重载成功后才确认。
    """

    now = time.time()

    active = {
        ip: expiry
        for ip, expiry in banned_until.items()
        if expiry > now
    }

    old_content = read_blacklist()

    new_content = "".join(
        "{} 1;\n".format(ip)
        for ip insorted(active)
    )

    if old_content == new_content:
        banned_until.clear()
        banned_until.update(active)
        save_state()
        returnTrue

    tmp = BLACKLIST_FILE + ".tmp"

    os.makedirs(os.path.dirname(BLACKLIST_FILE), exist_ok=True)

    withopen(tmp, "w") as f:
        f.write(new_content)

    os.replace(tmp, BLACKLIST_FILE)

    if reload_nginx():
        banned_until.clear()
        banned_until.update(active)
        save_state()
        print("自动黑名单已更新", flush=True)
        returnTrue

    # 检查或重载失败时恢复旧文件
    withopen(BLACKLIST_FILE, "w") as f:
        f.write(old_content)

    print(
        "Nginx 检查或重载失败,已恢复原黑名单文件;"
        "请检查错误日志",
        flush=True
    )

    returnFalse


defparse_line(line):
    match = LOG_RE.match(line)

    ifnotmatch:
        returnNone

    ip, timestamp, request = match.groups()

    try:
        ipaddress.ip_address(ip)

        dt = datetime.strptime(
            timestamp,
            "%d/%b/%Y:%H:%M:%S %z"
        )

        event_time = dt.timestamp()

    except (ValueError, OverflowError):
        returnNone

    parts = request.split()

    iflen(parts) < 2:
        returnNone

    path = parts[1].split("?", 1)[0]

    ifnot path.startswith("/api/"):
        returnNone

    # 指定接口不参与自动封禁请求次数统计
    # 同时兼容带查询参数的请求,例如 ?t=123
    if path in EXCLUDED_PATHS:
        returnNone

    return ip, event_time


deffollow_log():
    """
    初次启动从日志末尾开始,不统计历史记录。
    发生日志轮转时,从新日志开头继续读取。
    """

    f = None
    inode = None

    whileTrue:
        try:
            st = os.stat(LOG_FILE)

            if f isNone:
                f = open(LOG_FILE, "r", encoding="utf-8", errors="replace")
                f.seek(0, os.SEEK_END)
                inode = os.fstat(f.fileno()).st_ino

            elif st.st_ino != inode:
                f.close()
                f = open(LOG_FILE, "r", encoding="utf-8", errors="replace")
                inode = os.fstat(f.fileno()).st_ino

            elif st.st_size < f.tell():
                f.seek(0)

            line = f.readline()

            ifnot line:
                time.sleep(0.2)
                yieldNone
                continue

            yield line

        except FileNotFoundError:
            time.sleep(1)

        except OSError as e:
            print("读取日志失败: {}".format(e), flush=True)
            time.sleep(1)


defmain():
    load_state()

    # 启动时同步封禁状态。
    # 若需要恢复尚未到期的封禁,状态文件会保留对应时间。
    sync_blacklist()

    print(
        "自动 IP 黑名单服务已启动,只统计 /api/ 请求",
        flush=True
    )

    last_sync = 0

    for line in follow_log():
        now = time.time()

        if line:
            event = parse_line(line)

            if event:
                ip, event_time = event

                if ip notin WHITELIST:
                    if banned_until.get(ip, 0) <= now:
                        q = requests[ip]
                        q.append(event_time)

                        cutoff = event_time - WINDOW_SECONDS

                        while q and q[0] < cutoff:
                            q.popleft()

                        iflen(q) > THRESHOLD:
                            try:
                                ipaddress.ip_address(ip)
                                banned_until[ip] = now + BAN_SECONDS

                                requests.pop(ip, None)

                                print(
                                    "封禁 IP: {},"
                                    "滚动 60 秒内超过 {} 次 /api/ 请求"
                                    .format(ip, THRESHOLD),
                                    flush=True
                                )

                            except ValueError:
                                pass

        # 每秒同步一次,处理新封禁与过期解封。
        if now - last_sync >= 1:
            for ip, queue inlist(requests.items()):
                cutoff = now - WINDOW_SECONDS

                while queue and queue[0] < cutoff:
                    queue.popleft()

                ifnot queue:
                    requests.pop(ip, None)

            sync_blacklist()
            last_sync = now


if __name__ == "__main__":
    main()

3、检查脚本语法

/usr/bin/python3 -m py_compile /usr/local/nginx/scripts/auto_ip_blacklist.py

4、存后设置权限

chmod 750 /usr/local/nginx/scripts/auto_ip_blacklist.py

重要说明

  1. 1. 程序使用独立的状态文件记录封禁到期时间,避免服务重启后把已封禁 IP 立即解除。
  2. 2. 首次启动只统计之后新产生的日志,不会直接扫描历史日志封禁 IP。
  3. 3. 程序需要以 root 身份运行,因为它要修改 Nginx 配置包含文件并执行 Nginx reload。
  4. 4. 如果你有固定管理公网 IP,应将其加入 WHITELIST,并确保该地址是可信的真实客户端 IP。
  5. 5. 可以配置指定接口不参与 IP 请求次数统计,应将其加入 EXCLUDED_PATHS。
  6. 6. 不要同时运行多个相同的自动封禁进程,否则可能造成文件写入和状态竞争。

注意: 脚本启动时从访问日志末尾开始统计,只计算启动后的新请求,不会因为历史日志里已经存在大量请求而直接封禁。

配置 systemd 守护服务

创建服务文件(包含root账号和nginx的不同配置,根据业务选择即可):

vim /etc/systemd/system/nginx-auto-blacklist.service

# 
确认 Python 3 的实际路径:如果不是 /usr/bin/python3,请修改 ExecStart。

command -v python3

# 
内容 root 账号

[Unit]
Description=Nginx Automatic IP Blacklist
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/python3 /usr/local/nginx/scripts/auto_ip_blacklist.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

# 
内容 nginx 账号
[Unit]
Description=Nginx automatic IP blacklist service
After=network.target nginx.service
Requires=nginx.service

[Service]
Type=simple
User=nginx
Group=nginx
ExecStart=/usr/bin/python3 -u /usr/local/nginx/scripts/auto_ip_blacklist.py
Restart=always
RestartSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target

启用服务:

systemctl daemon-reload
systemctl enable --now nginx-auto-blacklist

重启自动封禁服务:

systemctl restart nginx-auto-blacklist

查看运行状态:

systemctl status nginx-auto-blacklist

实时查看封禁记录:

journalctl -u nginx-auto-blacklist -f

查看最近 100 行:

journalctl -u nginx-auto-blacklist -n 100 --no-pager

查看自动黑名单:

cat /usr/local/nginx/conf/blacklist/auto_ip_blacklist.conf

查看请求是否进入访问日志

tail -n 100 /usr/local/nginx/logs/access.log | grep ' /api/'

tail -n 200 /usr/local/nginx/logs/access.log | grep ' /api/' | tail -n 20

验证封禁是否生效

1. 检查 Nginx 配置

/usr/local/nginx/sbin/nginx -t

2. 检查自动黑名单

cat /usr/local/nginx/conf/blacklist/auto_ip_blacklist.conf

触发封禁后,应该能看到类似:

192.168.1.10 1;

这里仍然是示例 IP。

3. 查看访问日志

tail -f /usr/local/nginx/logs/access.log

4. 验证 HTTP 403

在被封禁 IP 对应的真实客户端发起请求,预期结果是:

HTTP/1.1 403 Forbidden

不要只在服务器本机测试,因为本机访问的源 IP 与公网客户端 IP 可能不同。

5. 手动解封

如果是手动黑名单中的 IP,删除 ip_blacklist.conf 中对应行后执行:

/usr/local/nginx/sbin/nginx -t &&
/usr/local/nginx/sbin/nginx -s reload

如果是自动封禁,默认等待 1 小时即可自动解除。需要立即解除时,应先停止自动封禁服务,再修改自动黑名单和状态文件,之后重新加载 Nginx 并启动服务,避免程序立刻重新写入封禁记录。

故障排查

1. unknown directive "if($blacklist_ip"

确保写成:

if ($blacklist_ip = 1) {
    return 403;
}

2. 黑名单文件不存在

执行:

ls -l /usr/local/nginx/conf/blacklist/

确保两个 .conf 文件都存在。

3. 配置文件检查失败

执行:

/usr/local/nginx/sbin/nginx -t

如果失败,不要继续 reload。先根据报错的文件路径和行号修复配置。

4. 自动封禁服务启动失败

执行:

journalctl -u nginx-auto-blacklist -n 100 --no-pager

同时检查:

ls -l /usr/local/nginx/logs/access.log
ls -l /usr/local/nginx/sbin/nginx

5. 没有触发封禁

确认以下条件:

  • • 服务处于 active (running) 状态。
  • • 访问日志持续有新增请求。
  • • 请求路径以 /api/ 开头。
  • • 同一个客户端 IP 在 60 秒内达到 61 次请求。
  • • 客户端 IP 不在白名单中。
  • • Nginx 实际加载了两个黑名单文件。
  • • Nginx 的访问日志记录的是实际客户端 IP,而不是 CDN 或代理 IP。

安全注意事项

  • • 如果 Nginx 位于 CDN 或负载均衡之后,必须先配置可信代理 IP 范围,再正确获取客户端 IP;不能直接信任客户端任意传入的 X-Forwarded-For。
  • • 不建议把所有自动封禁 IP 永久保留,否则共享公网 IP、移动端用户、公司 NAT 出口用户可能长期无法访问。
  • • Nginx 的 limit_req 可以作为第一层限流,自动黑名单作为第二层措施;两者并不互相替代。
  • • 自动封禁服务和 Nginx 配置文件应限制写权限,避免普通用户修改黑名单或脚本。
  • • 建议先在测试站点观察一段时间,确认误封率可接受,再用于所有生产站点。

部署检查清单

  • • geo $blacklist_ip 已放在 http {} 中。
  • • 手动与自动黑名单文件均存在。
  • • 需要保护的 server {} 均配置了拦截规则。
  • • nginx -t 检查通过。
  • • Nginx 已成功 reload。
  • • Python 3 脚本已保存并具有执行权限。
  • • systemd 服务已启用并处于运行状态。
  • • 已验证黑名单 IP 返回 HTTP 403。
  • • 已验证自动封禁记录及到期解封。
  • • 已检查代理链路、白名单和误封风险。


打赏

本文链接:https://kinber.cn/post/6810.html 转载需授权!

分享到:


推荐本站淘宝优惠价购买喜欢的宝贝:

image.png

 您阅读本篇文章共花了: 

群贤毕至

访客