feat(security): 添加fail2ban集成防止暴力破解登录

- 新增fail2ban过滤器和监狱配置文件
- 修改登录视图记录客户端IP和认证失败日志
- 更新日志配置添加syslog处理器用于fail2ban检测
- 在README中添加fail2ban配置和使用文档
This commit is contained in:
2026-01-28 22:44:36 +08:00
parent f7692a6db6
commit ae95844177
5 changed files with 411 additions and 9 deletions

283
README.md
View File

@@ -1625,6 +1625,287 @@ print('初始化完成')
**解决方法**确认使用的是POST方法不是GET方法 **解决方法**确认使用的是POST方法不是GET方法
## Fail2ban 登录保护配置
为了防止暴力破解登录密码,系统集成了 Fail2ban 自动封禁功能。当用户在短时间内多次登录失败时,其 IP 地址将被自动封禁。
### 工作原理
1. **日志记录**:登录失败时,系统会记录包含 IP 地址的警告日志到 syslog 和 `logs/auth.log`
2. **Fail2ban 监控**Fail2ban 实时监控日志文件,检测登录失败事件
3. **自动封禁**:当同一 IP 在指定时间内失败次数超过阈值时,自动封禁该 IP
### 安装 Fail2ban
在 Ubuntu 服务器上安装 Fail2ban
```bash
# 更新包管理器
sudo apt update
# 安装 Fail2ban
sudo apt install fail2ban -y
# 启动 Fail2ban 服务
sudo systemctl start fail2ban
# 设置开机自启
sudo systemctl enable fail2ban
# 检查服务状态
sudo systemctl status fail2ban
```
### 配置 Fail2ban
#### 1. 复制过滤器配置
```bash
# 创建过滤器目录(如果不存在)
sudo mkdir -p /etc/fail2ban/filter.d
# 复制过滤器配置
sudo cp /var/www/diary-family/deploy/fail2ban/filter.d/diary-family.conf /etc/fail2ban/filter.d/
# 验证过滤器配置
sudo fail2ban-client -t
```
#### 2. 复制监狱配置
```bash
# 创建监狱配置目录(如果不存在)
sudo mkdir -p /etc/fail2ban/jail.d
# 复制监狱配置
sudo cp /var/www/diary-family/deploy/fail2ban/jail.d/diary-family.conf /etc/fail2ban/jail.d/
# 编辑配置(根据实际需求修改)
sudo nano /etc/fail2ban/jail.d/diary-family.conf
```
#### 3. 关键配置参数说明
| 参数 | 默认值 | 说明 |
|-----|-------|------|
| `maxretry` | 5 | 触发封禁前的最大失败次数 |
| `bantime` | 3600 | 封禁时间默认1小时 |
| `findtime` | 600 | 检测时间窗口默认10分钟 |
| `ignoreip` | 127.0.0.1 | 白名单IP不会被封禁 |
#### 4. 重启 Fail2ban 服务
```bash
# 重新加载配置
sudo systemctl restart fail2ban
# 或者使用 fail2ban-client
sudo fail2ban-client reload
```
### 验证配置
#### 1. 检查 Fail2ban 状态
```bash
# 查看整体状态
sudo fail2ban-client status
# 查看 diary-family 监狱状态
sudo fail2ban-client status diary-family
# 查看被封禁的IP列表
sudo fail2ban-client status diary-family | grep "Banned IP list"
```
#### 2. 测试登录失败检测
```bash
# 手动测试过滤器(使用最近的日志)
sudo fail2ban-regex /var/log/syslog /etc/fail2ban/filter.d/diary-family.conf
# 或者测试 auth.log
sudo fail2ban-regex /var/www/diary-family/logs/auth.log /etc/fail2ban/filter.d/diary-family.conf
```
#### 3. 查看日志
```bash
# 查看 Fail2ban 日志
sudo tail -f /var/log/fail2ban.log
# 查看系统日志中的认证失败
sudo grep "Authentication failure" /var/log/syslog
# 查看 Django 认证日志
tail -f /var/www/diary-family/logs/auth.log
```
### 手动管理封禁
#### 解封 IP 地址
```bash
# 手动解封某个 IP
sudo fail2ban-client set diary-family unbanip 192.168.1.100
# 解封所有 IP
sudo fail2ban-client set diary-family unbanip --all
```
#### 封禁 IP 地址
```bash
# 手动封禁某个 IP
sudo fail2ban-client set diary-family banip 192.168.1.100
```
### 高级配置
#### 使用 UFW 作为防火墙后端
如果使用 UFW 防火墙,修改配置:
```bash
# 编辑监狱配置
sudo nano /etc/fail2ban/jail.d/diary-family.conf
# 修改 banaction
banaction = ufw
# 重启服务
sudo systemctl restart fail2ban
```
#### 配置邮件通知
启用邮件通知功能:
```bash
# 编辑监狱配置
sudo nano /etc/fail2ban/jail.d/diary-family.conf
# 添加邮件配置
destemail = admin@example.com
sender = fail2ban@example.com
mta = sendmail
action = %(action_mwl)s
# 重启服务
sudo systemctl restart fail2ban
```
#### 调整封禁策略
编辑 `/etc/fail2ban/jail.d/diary-family.conf`
```ini
# 更严格的策略3次失败封禁24小时
maxretry = 3
bantime = 86400
findtime = 300
# 或者更宽松的策略10次失败封禁30分钟
maxretry = 10
bantime = 1800
findtime = 900
```
### 故障排除
#### 问题1Fail2ban 无法启动
```bash
# 检查配置文件语法
sudo fail2ban-client -t
# 查看详细错误信息
sudo journalctl -u fail2ban -f
# 检查日志文件权限
ls -la /var/log/syslog
ls -la /var/www/diary-family/logs/auth.log
```
#### 问题2无法检测到登录失败
```bash
# 检查日志格式是否匹配
sudo fail2ban-regex /var/log/syslog /etc/fail2ban/filter.d/diary-family.conf
# 手动检查日志内容
grep "Authentication failure" /var/log/syslog
grep "Authentication failure" /var/www/diary-family/logs/auth.log
# 检查 Django 是否正确记录日志
tail -f /var/www/diary-family/logs/auth.log
```
#### 问题3IP 未被封禁
```bash
# 检查 iptables 规则
sudo iptables -L -n | grep fail2ban
# 检查封禁状态
sudo fail2ban-client status diary-family
# 检查日志中是否有封禁动作
sudo grep "Ban" /var/log/fail2ban.log
```
#### 问题4syslog 无法写入
如果使用 syslog 出现权限问题,可以只使用文件日志:
```bash
# 编辑监狱配置,只监控 auth.log
sudo nano /etc/fail2ban/jail.d/diary-family.conf
# 修改 logpath
logpath = /var/www/diary-family/logs/auth.log
# 重启服务
sudo systemctl restart fail2ban
```
### 安全建议
1. **合理设置阈值**:根据实际需求调整 `maxretry` 和 `findtime`,避免误封正常用户
2. **配置白名单**:将办公网络、家庭网络等添加到 `ignoreip`
3. **定期检查日志**:定期查看 `/var/log/fail2ban.log` 了解封禁情况
4. **设置永久封禁**:对于频繁攻击的 IP可以手动永久封禁或设置很长的 `bantime`
5. **监控异常**:结合邮件通知,及时了解安全事件
### 与 Nginx 集成(可选)
如果使用 Nginx 作为反向代理,可以额外配置 Nginx 日志监控:
```bash
# 创建 Nginx 过滤器
sudo tee /etc/fail2ban/filter.d/nginx-diary-family.conf << 'EOF'
[Definition]
failregex = ^<HOST> .* "POST /login/ HTTP/.*" (401|403|500)
^<HOST> .* "POST /login/ HTTP/.*" 200 .* "Invalid login"
ignoreregex =
EOF
# 添加到监狱配置
sudo tee -a /etc/fail2ban/jail.d/diary-family.conf << 'EOF'
[nginx-diary-family]
enabled = true
filter = nginx-diary-family
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 3600
findtime = 600
EOF
# 重启服务
sudo systemctl restart fail2ban
```
## 注意事项 ## 注意事项
1. 定期备份数据库 1. 定期备份数据库
@@ -1632,6 +1913,8 @@ print('初始化完成')
3. 定期清理过期数据 3. 定期清理过期数据
4. 保持依赖包更新 4. 保持依赖包更新
5. 定期运行邮件测试脚本,确保邮件功能正常 5. 定期运行邮件测试脚本,确保邮件功能正常
6. **Fail2ban 配置**:生产环境务必配置 Fail2ban 防止暴力破解
7. **白名单设置**:配置 `ignoreip` 避免误封自己的 IP
## 许可证 ## 许可证

View File

@@ -10,6 +10,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from datetime import timedelta, datetime from datetime import timedelta, datetime
import os import os
import logging
from loguru import logger from loguru import logger
# WeasyPrint可用性标记初始为None # WeasyPrint可用性标记初始为None
@@ -889,30 +890,42 @@ def api_submit_summary(request):
logger.error(f"API: 提交汇总记录失败: {str(e)}") logger.error(f"API: 提交汇总记录失败: {str(e)}")
return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500) return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500)
# 获取syslog日志记录器用于fail2ban检测
syslog_logger = logging.getLogger('django.security.login')
# 登录视图 # 登录视图
def user_login(request): def user_login(request):
"""用户登录""" """用户登录"""
if request.user.is_authenticated: if request.user.is_authenticated:
logger.info(f"用户 {request.user.username} 已登录,重定向到首页") logger.info(f"用户 {request.user.username} 已登录,重定向到首页")
return redirect('index') return redirect('index')
if request.method == 'POST': if request.method == 'POST':
username = request.POST.get('username') username = request.POST.get('username')
password = request.POST.get('password') password = request.POST.get('password')
logger.info(f"用户登录尝试: {username}") # 获取客户端IP地址
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
client_ip = x_forwarded_for.split(',')[0].strip()
else:
client_ip = request.META.get('REMOTE_ADDR', 'unknown')
logger.info(f"用户登录尝试: {username}, IP: {client_ip}")
user = authenticate(request, username=username, password=password) user = authenticate(request, username=username, password=password)
if user is not None: if user is not None:
login(request, user) login(request, user)
logger.info(f"用户 {username} 登录成功") logger.info(f"用户 {username} 登录成功, IP: {client_ip}")
messages.success(request, '登录成功!') messages.success(request, '登录成功!')
return redirect('index') return redirect('index')
else: else:
logger.warning(f"用户 {username} 登录失败: 用户名或密码错误") logger.warning(f"用户 {username} 登录失败: 用户名或密码错误, IP: {client_ip}")
# 记录到syslog供fail2ban检测
syslog_logger.warning(f"Authentication failure for username: {username} from IP: {client_ip}")
messages.error(request, '用户名或密码错误,请重新尝试。') messages.error(request, '用户名或密码错误,请重新尝试。')
return render(request, 'core/login.html') return render(request, 'core/login.html')
# 注销视图 # 注销视图

View File

@@ -0,0 +1,20 @@
# Fail2Ban filter for diary-family Django application
# 用于检测家庭日报系统登录失败的规则
[Definition]
# 匹配登录失败的日志行
# 日志格式: django.security.login: WARNING Authentication failure for username: xxx from IP: xxx.xxx.xxx.xxx
failregex = ^.*Authentication failure for username: .* from IP: <HOST>.*$
# 可选:匹配其他认证失败模式(如被禁用的用户)
# failregex = ^%(__prefix_line)s.*Authentication failure for username: .* from IP: <HOST>.*$
# ^%(__prefix_line)s.*Invalid login attempt from IP: <HOST>.*$
# 忽略正则(可选)
# ignoreregex =
[Init]
# 日期格式(如果需要)
# datepattern = %%Y-%%m-%%d %%H:%%M:%%S

View File

@@ -0,0 +1,62 @@
# Fail2Ban jail configuration for diary-family Django application
# 家庭日报系统 fail2ban 监狱配置
[diary-family]
# 启用该监狱
enabled = true
# 监狱名称和过滤器
filter = diary-family
# 要监控的日志文件路径
# 如果使用syslog可以设置为 /var/log/syslog 或 /var/log/messages
# 如果使用文件日志,设置为 Django 项目的 auth.log 路径
logpath = /var/log/syslog
/var/www/diary-family/logs/auth.log
# 触发封禁前的最大失败次数
maxretry = 5
# 封禁时间(秒)- 默认1小时3600秒
# 可以设置为:
# - 3600 = 1小时
# - 86400 = 1天
# - 604800 = 1周
# - -1 = 永久封禁
bantime = 3600
# 检测时间窗口(秒)- 在多长时间内计算失败次数
# 例如findtime = 600 表示在10分钟内失败 maxretry 次则封禁
findtime = 600
# 忽略的IP地址白名单
# 可以添加本地网络、办公网络等不需要封禁的IP
ignoreip = 127.0.0.1/8 ::1
# 192.168.1.0/24 # 本地网络示例
# 10.0.0.0/8 # 内网示例
# 使用的封禁动作
# 默认使用 iptables 封禁所有端口
# 可选动作:
# - iptables-allports: 封禁所有端口
# - iptables-multiport: 封禁指定端口
# - nftables-allports: 使用 nftables 封禁
# - ufw: 使用 UFW 防火墙
# - cloudflare: 封禁 Cloudflare 上的IP
banaction = iptables-allports
# 发送邮件通知(可选)
# 需要配置邮件服务器
# destemail = admin@example.com
# sender = fail2ban@example.com
# mta = sendmail
# action = %(action_mwl)s # 发送邮件并记录日志
# 后端选择
# - auto: 自动选择(推荐)
# - systemd: 使用 systemd journal
# - polling: 轮询模式
backend = auto
# 端口(用于 multiport 动作)
port = 80,443,8000

View File

@@ -196,13 +196,16 @@ LOGGING = {
'format': '[%(asctime)s] [%(levelname)s] [%(process)d] [%(module)s] %(message)s', 'format': '[%(asctime)s] [%(levelname)s] [%(process)d] [%(module)s] %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S' 'datefmt': '%Y-%m-%d %H:%M:%S'
}, },
'syslog': { # syslog格式用于fail2ban检测
'format': '%(name)s: %(levelname)s %(message)s'
},
}, },
'handlers': { 'handlers': {
'file': { # 日志写入文件的处理器 'file': { # 日志写入文件的处理器
'level': 'INFO', # 日志级别INFO及以上都记录ERROR/WARNING/INFO 'level': 'INFO', # 日志级别INFO及以上都记录ERROR/WARNING/INFO
'class': 'logging.handlers.RotatingFileHandler', # 日志轮转,防止文件过大 'class': 'logging.handlers.RotatingFileHandler', # 日志轮转,防止文件过大
# ✅ 核心pathlib对象转字符串logging只接收字符串路径必转 # ✅ 核心pathlib对象转字符串logging只接收字符串路径必转
'filename': str(LOG_DIR / 'all_in_one.log'), 'filename': str(LOG_DIR / 'all_in_one.log'),
'maxBytes': 1024 * 1024 * 100, # 单个日志文件最大100MB 'maxBytes': 1024 * 1024 * 100, # 单个日志文件最大100MB
'backupCount': 10, # 最多保留10个日志备份 'backupCount': 10, # 最多保留10个日志备份
'formatter': 'standard', # 使用上面定义的统一格式 'formatter': 'standard', # 使用上面定义的统一格式
@@ -213,6 +216,22 @@ LOGGING = {
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'standard' 'formatter': 'standard'
}, },
'syslog': { # syslog处理器用于fail2ban检测登录失败
'level': 'WARNING',
'class': 'logging.handlers.SysLogHandler',
'address': '/dev/log', # Linux系统日志socket
'facility': 'local0',
'formatter': 'syslog',
},
'auth_file': { # 认证日志文件处理器(备选方案)
'level': 'WARNING',
'class': 'logging.handlers.RotatingFileHandler',
'filename': str(LOG_DIR / 'auth.log'),
'maxBytes': 1024 * 1024 * 50, # 50MB
'backupCount': 5,
'formatter': 'standard',
'encoding': 'utf-8',
},
}, },
# 所有日志器配置和原配置完全一致,无需任何修改 # 所有日志器配置和原配置完全一致,无需任何修改
'loggers': { 'loggers': {
@@ -226,6 +245,11 @@ LOGGING = {
'level': 'INFO', 'level': 'INFO',
'propagate': True, 'propagate': True,
}, },
'django.security.login': { # 登录安全日志用于fail2ban
'handlers': ['syslog', 'auth_file'],
'level': 'WARNING',
'propagate': False,
},
'celery': { # Celery客户端日志Django中提交任务的日志 'celery': { # Celery客户端日志Django中提交任务的日志
'handlers': ['file'], 'handlers': ['file'],
'level': 'INFO', 'level': 'INFO',