在 Rust Axum 后端开发中,我们常面临这样的困境:代码里检测到了恶意爬虫或暴力破解,但仅仅返回 403 Forbidden 是不够的——攻击者依然占用着带宽和连接数。

虽然我们可以用 iptables 库在 Rust 里直接封禁 IP,但这会导致权限耦合和性能损耗。最优雅的方案是“各司其职”:Axum 负责业务逻辑判断,Nginx 负责流量清洗,而 Fail2ban 负责充当“自动保镖”,根据日志动态封禁 IP。

本文将手把手教你如何通过 日志驱动 的方式,实现毫秒级响应的 IP 自动封禁。


🏗️ 核心架构原理

我们采用 日志驱动(Log-Driven) 的解耦架构:

  1. Axum (应用层):检测到恶意行为(如撞库、越权),打印一条特定格式的日志
  2. Fail2ban (监控层):实时监控 Nginx/Axum 的日志文件。
  3. Iptables (内核层):一旦匹配到特定日志模式,Fail2ban 调用系统防火墙直接丢弃该 IP 的数据包。

优势:Axum 无需 Root 权限,无需关心网络层细节,只需专注于“什么是恶意请求”。


🛠️ 第一步:Axum 端埋点

首先,我们需要在 Axum 中定义“恶意行为”。为了不让 Fail2ban 误判,我们最好输出一个独特的标识。

假设你使用 tracing crate,我们可以自定义一个中间件或在路由处理器中增加逻辑:

use axum::{
    extract::ConnectInfo,
    http::StatusCode,
    response::IntoResponse,
    routing::post,
    Router,
};
use std::net::SocketAddr;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

// 模拟登录接口
async fn login(
    ConnectInfo(addr): ConnectInfo<SocketAddr>,
    // 这里应该注入你的业务逻辑 State
) -> impl IntoResponse {
    // 模拟检测到恶意行为(例如:密码错误超过5次)
    let is_malicious = true; 

    if is_malicious {
        // 🔴 关键点:打印特定格式的日志
        // 格式建议:[BLOCK_ME] IP:xxx  这样方便正则匹配且不易误伤
        tracing::warn!(target: "security", "[BLOCK_ME] IP: {}", addr.ip());
        return StatusCode::UNAUTHORIZED;
    }

    StatusCode::OK
}

#[tokio::main]
async fn main() {
    // 初始化日志,确保日志输出到文件或 stdout 被 Nginx 捕获
    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer())
        .init();

    let app = Router::new()
        .route("/login", post(login))
        .layer(TraceLayer::new_for_http())
        .into_make_service_with_connect_info::<SocketAddr>();

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app)
        .await
        .unwrap();
}

⚙️ 第二步:Nginx 配置

Fail2ban 需要读取日志。如果你的 Axum 运行在 Docker 或后端端口,Nginx 作为反向代理,我们需要确保 Nginx 记录了真实的客户端 IP,并且日志格式包含我们需要匹配的内容。

编辑 /etc/nginx/nginx.conf 或站点配置文件:

http {
    # 1. 定义日志格式,确保记录 $remote_addr (真实IP)
    # 注意:如果你使用了 Docker,确保 Nginx 配置了 set_real_ip_from 以获取真实 IP
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    server {
        listen 80;
        server_name your_domain.com;

        location / {
            proxy_pass http://127.0.0.1:3000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr; # 传递真实 IP
        }
    }
}

关键点:Axum 打印的日志通常会通过 Docker 的 stdout 或单独的文件输出。

  • 情况 A(推荐):Axum 日志输出到 /var/log/axum/app.log。Fail2ban 直接监控这个文件。
  • 情况 B:Axum 日志被 Nginx 代理并写入 /var/log/nginx/access.log。Fail2ban 监控 Nginx 日志。

下文以 情况 A(直接监控应用日志) 为例,因为这样更精准,不受 Nginx 格式干扰。


🕵️ 第三步:编写 Fail2ban 过滤器

我们需要告诉 Fail2ban 什么样的日志算“恶意”。

创建文件:/etc/fail2ban/filter.d/axum-malicious.conf

[Definition]
# failregex 是正则表达式,用于匹配恶意日志
# ^<HOST> 是 Fail2ban 的占位符,自动匹配行首的 IP 地址
# 我们匹配 Axum 打印的 "[BLOCK_ME]" 标识
failregex = ^<HOST>.*$$BLOCK_ME$$.*$

# ignoreregex 用于排除误判,通常留空或排除自己的监控 IP
ignoreregex =

🚔 第四步:配置监狱

这是 Fail2ban 的核心,定义了“怎么封”、“封多久”。

创建文件:/etc/fail2ban/jail.d/axum-local.conf

[axum-malicious]
enabled = true
port = http,https
# 指定刚才创建的过滤器
filter = axum-malicious
# 🔴 关键:指向 Axum 的日志文件路径
logpath = /var/log/axum/app.log
# 也可以监控 Nginx 日志,如果是这种情况:
# logpath = /var/log/nginx/access.log

# 封禁策略:
# maxretry: 在 findtime 秒内超过多少次触发
maxretry = 3
# findtime: 时间窗口(秒)
findtime = 60
# bantime: 封禁时长(秒),3600 = 1小时
bantime = 3600

# 白名单:永远不要封禁自己!
ignoreip = 127.0.0.1/8 ::1 YOUR_SERVER_IP

🚀 第五步:启动与验证

  1. 重启 Fail2ban

    sudo systemctl restart fail2ban
    
  2. 检查状态
    查看监狱是否启动成功:

    sudo fail2ban-client status axum-malicious
    

    输出应显示 StatusOK,且 Currently banned 为 0。

  3. 实战模拟
    在你的本地机器(非白名单 IP)上,快速请求 3 次恶意接口:

    # 模拟攻击
    for i in {1..5}; do curl -X POST http://your-server/login; done
    
  4. 观察结果
    再次运行状态命令:

    sudo fail2ban-client status axum-malicious
    

    你应该能看到你的攻击者 IP 出现在 Banned IP list 中。此时,该 IP 的所有请求(包括正常请求)都会被服务器直接丢弃(Connection Timed Out)。


💡 进阶技巧 与 避坑指南

1. Fail2ban 的 安装 和 启动

在 CentOS 系统上安装 Fail2ban 非常直接,它的主要依赖(如 Python)通常已预装,核心步骤是启用 EPEL 软件仓库。

📦 安装 Fail2ban

CentOS 的官方软件源不包含 Fail2ban,因此我们需要先安装 EPEL (Extra Packages for Enterprise Linux) 仓库。

请根据你的 CentOS 版本选择对应的命令:

对于 CentOS 7
# 1. 安装 EPEL 仓库
sudo yum install epel-release -y

# 2. 安装 Fail2ban
sudo yum install fail2ban -y
对于 CentOS 8 / Stream / Rocky Linux / AlmaLinux
# 1. 安装 EPEL 仓库
sudo dnf install epel-release -y

# 2. 安装 Fail2ban
sudo dnf install fail2ban -y

⚙️ 启动与配置

安装完成后,Fail2ban 服务默认不会自动启动,需要手动操作。

1. 启动服务并设置开机自启
# 启动 Fail2ban 服务
sudo systemctl start fail2ban

# 设置开机自动启动
sudo systemctl enable fail2ban

# 检查服务状态,确认是否为 "active (running)"
sudo systemctl status fail2ban
2. 创建 自定义配置文件 (关键步骤)

重要原则永远不要直接修改 /etc/fail2ban/jail.conf,因为软件更新会覆盖你的修改。
所有自定义配置都应放在 /etc/fail2ban/jail.local 文件中。

我们可以 复制默认配置文件 作为 模板 来开始:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

然后,使用你喜欢的编辑器(如 vimnano)打开这个文件:

sudo vim /etc/fail2ban/jail.local

2. 邮件告警

不想时刻盯着控制台?配置 Fail2ban 发送邮件。
jail.d/axum-local.conf[DEFAULT] 部分添加:

destemail = admin@yourcompany.com
sender = fail2ban@your-server.com
mta = sendmail
# action_mwl = 封禁 + 发送带日志的邮件
action = %(action_mwl)s

3. 误封怎么办?

如果测试时把自己封了,或者规则太严误伤了用户:

# 手动解封指定 IP
sudo fail2ban-client set axum-malicious unbanip 192.168.1.50

4. Docker 环境特别注意

如果你的 Axum 运行在 Docker 容器里,日志文件在容器内部。Fail2ban 运行在宿主机,无法直接读取。
解决方案
docker rundocker-compose.yml 中,将容器内的日志文件挂载到宿主机:

services:
  axum-app:
    image: my-axum-app
    volumes:
      # 将容器内的日志挂载到宿主机的 /var/log/axum/app.log
      - ./app.log:/var/log/axum/app.log

5. 配置 SSH 防护

在配置文件中找到 [sshd] 部分,并修改或确保包含以下内容:

[DEFAULT]
# 白名单:务必将你自己的 IP 地址加入,防止把自己锁在外面!
ignoreip = 127.0.0.1/8 ::1 你的公网IP地址

# 全局默认设置
bantime  = 3600  # 封禁时长(秒),这里是1小时
findtime = 600   # 检测时间窗口(秒)
maxretry = 5     # 最大失败尝试次数

# SSH 服务防护配置
[sshd]
enabled = true
# CentOS 的 SSH 日志文件路径
logpath = /var/log/secure

配置完成后,保存并退出编辑器。

重新加载配置

使你的新配置生效:

sudo systemctl restart fail2ban

🧪 验证与使用

配置完成后,你可以通过以下命令来管理和验证 Fail2ban。

1. 查看 SSH 防护状态
sudo fail2ban-client status sshd

这个命令会显示当前有多少 IP 被封禁,以及封禁列表。

2. 手动解封 IP

如果你不小心把自己封禁了,或者需要解封某个 IP,可以使用:

sudo fail2ban-client set sshd unbanip <被封禁的IP地址>
3. 测试封禁规则

你可以从另一台机器故意输错 5 次 SSH 密码,然后回到服务器上使用 status 命令查看该 IP 是否已被成功封禁。

📝 总结

通过这套方案,我们实现了一个低成本、高内聚的安全防御体系:

  1. Axum 保持纯粹,只负责识别业务层面的恶意行为。
  2. Fail2ban 负责脏活累活,在系统内核层阻断连接。
  3. Nginx 继续高性能地处理合法流量。

这比在 Rust 代码里手写 iptables 命令要稳健得多,也更符合云原生时代的运维标准。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐