diff --git a/README.md b/README.md index 13009af..45e4cca 100644 --- a/README.md +++ b/README.md @@ -1625,6 +1625,287 @@ print('初始化完成') **解决方法**:确认使用的是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 +``` + +### 故障排除 + +#### 问题1:Fail2ban 无法启动 + +```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 +``` + +#### 问题3:IP 未被封禁 + +```bash +# 检查 iptables 规则 +sudo iptables -L -n | grep fail2ban + +# 检查封禁状态 +sudo fail2ban-client status diary-family + +# 检查日志中是否有封禁动作 +sudo grep "Ban" /var/log/fail2ban.log +``` + +#### 问题4:syslog 无法写入 + +如果使用 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 = ^ .* "POST /login/ HTTP/.*" (401|403|500) + ^ .* "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. 定期备份数据库 @@ -1632,6 +1913,8 @@ print('初始化完成') 3. 定期清理过期数据 4. 保持依赖包更新 5. 定期运行邮件测试脚本,确保邮件功能正常 +6. **Fail2ban 配置**:生产环境务必配置 Fail2ban 防止暴力破解 +7. **白名单设置**:配置 `ignoreip` 避免误封自己的 IP ## 许可证 diff --git a/core/views.py b/core/views.py index 08c056a..c639b30 100644 --- a/core/views.py +++ b/core/views.py @@ -10,6 +10,7 @@ from django.contrib.auth.decorators import login_required from django.contrib import messages from datetime import timedelta, datetime import os +import logging from loguru import logger # WeasyPrint可用性标记,初始为None @@ -889,30 +890,42 @@ def api_submit_summary(request): logger.error(f"API: 提交汇总记录失败: {str(e)}") return JsonResponse({'success': False, 'message': f"提交失败: {str(e)}"}, status=500) +# 获取syslog日志记录器(用于fail2ban检测) +syslog_logger = logging.getLogger('django.security.login') + # 登录视图 def user_login(request): """用户登录""" if request.user.is_authenticated: logger.info(f"用户 {request.user.username} 已登录,重定向到首页") return redirect('index') - + if request.method == 'POST': username = request.POST.get('username') 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) - + if user is not None: login(request, user) - logger.info(f"用户 {username} 登录成功") + logger.info(f"用户 {username} 登录成功, IP: {client_ip}") messages.success(request, '登录成功!') return redirect('index') 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, '用户名或密码错误,请重新尝试。') - + return render(request, 'core/login.html') # 注销视图 diff --git a/deploy/fail2ban/filter.d/diary-family.conf b/deploy/fail2ban/filter.d/diary-family.conf new file mode 100644 index 0000000..676150f --- /dev/null +++ b/deploy/fail2ban/filter.d/diary-family.conf @@ -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: .*$ + +# 可选:匹配其他认证失败模式(如被禁用的用户) +# failregex = ^%(__prefix_line)s.*Authentication failure for username: .* from IP: .*$ +# ^%(__prefix_line)s.*Invalid login attempt from IP: .*$ + +# 忽略正则(可选) +# ignoreregex = + +[Init] + +# 日期格式(如果需要) +# datepattern = %%Y-%%m-%%d %%H:%%M:%%S diff --git a/deploy/fail2ban/jail.d/diary-family.conf b/deploy/fail2ban/jail.d/diary-family.conf new file mode 100644 index 0000000..6b3acb8 --- /dev/null +++ b/deploy/fail2ban/jail.d/diary-family.conf @@ -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 diff --git a/diary_family/settings.py b/diary_family/settings.py index 20bdd46..e80d88f 100644 --- a/diary_family/settings.py +++ b/diary_family/settings.py @@ -196,13 +196,16 @@ LOGGING = { 'format': '[%(asctime)s] [%(levelname)s] [%(process)d] [%(module)s] %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S' }, + 'syslog': { # syslog格式(用于fail2ban检测) + 'format': '%(name)s: %(levelname)s %(message)s' + }, }, 'handlers': { 'file': { # 日志写入文件的处理器 'level': 'INFO', # 日志级别:INFO及以上都记录(ERROR/WARNING/INFO) 'class': 'logging.handlers.RotatingFileHandler', # 日志轮转,防止文件过大 # ✅ 核心:pathlib对象转字符串,logging只接收字符串路径,必转! - 'filename': str(LOG_DIR / 'all_in_one.log'), + 'filename': str(LOG_DIR / 'all_in_one.log'), 'maxBytes': 1024 * 1024 * 100, # 单个日志文件最大100MB 'backupCount': 10, # 最多保留10个日志备份 'formatter': 'standard', # 使用上面定义的统一格式 @@ -213,6 +216,22 @@ LOGGING = { 'class': 'logging.StreamHandler', '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': { @@ -226,6 +245,11 @@ LOGGING = { 'level': 'INFO', 'propagate': True, }, + 'django.security.login': { # 登录安全日志(用于fail2ban) + 'handlers': ['syslog', 'auth_file'], + 'level': 'WARNING', + 'propagate': False, + }, 'celery': { # Celery客户端日志(Django中提交任务的日志) 'handlers': ['file'], 'level': 'INFO',