服务器的 Nginx IP 黑名单完整配置,包括手动封禁、自动封禁、滚动 60 秒请求计数、封禁到期、systemd 守护运行、验证与故障排查。
geo 放在 http {} 中,if 判断放在各个 server {} 中;自动封禁程序只修改独立的自动黑名单文件,不覆盖手动黑名单。
统一使用以下路径:
/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
请求处理流程:
• 客户端请求进入 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. 程序使用独立的状态文件记录封禁到期时间,避免服务重启后把已封禁 IP 立即解除。 2. 首次启动只统计之后新产生的日志,不会直接扫描历史日志封禁 IP。 3. 程序需要以 root 身份运行,因为它要修改 Nginx 配置包含文件并执行 Nginx reload。 4. 如果你有固定管理公网 IP,应将其加入 WHITELIST,并确保该地址是可信的真实客户端 IP。5. 可以配置指定接口不参与 IP 请求次数统计,应将其加入 EXCLUDED_PATHS。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/nginx5. 没有触发封禁
确认以下条件:
• 服务处于 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://www.kinber.cn/post/6810.html 转载需授权!
推荐本站淘宝优惠价购买喜欢的宝贝:

支付宝微信扫一扫,打赏作者吧~
