导读:本期聚焦于南京GEO公司创作的《怎样配置 Rack Attack 的 Fail2Ban 过滤器让 blocklist 自动封禁 IP?》,敬请观看详情。Rack 应用开放登录接口后,暴力破解与撞库流量往往不会在短时间内触发普通节流,反而会绕过基于固定频率的限制。Rack Attack 提供的 Fail2Ban 能力可以在缓存中记录失败次数,达到阈值后通过 blocklist 规则直接封禁对应 IP,无需修改业务路由。本文从 blocklist 与 Fail2Ban filter 的协作机制讲起,演示初始化器、登录失败计数、诱捕路径过滤以及多规则拆分等配置方法,并说明 findtime、maxretry、bantime 三个参数对误封率和封禁时长的影响。文中还给出共享缓存和手动解封的运维建议,帮助在 Rails 或 Rack 应用中落地一套可调、可观测的 IP 黑名单过滤方案。

Rack::Attack 的 Fail2Ban 不是独立封禁引擎,而是在 Rack::Attack.cache 中维护计数,再由 blocklist 规则返回 true 来实现拦截。标题中的 Filter::Blocklist 并不对应实际的类路径,正确组合是 Rack::Attack.blocklist 块内调用 Rack::Attack::Fail2Ban.filter。因此配置黑名单的关键,是让 filter 的缓存 key、失败判定条件和封禁时长保持一致,否则会出现计数失效或误封。

怎样配置 Rack Attack 的 Fail2Ban 过滤器让 blocklist 自动封禁 IP?

Fail2Ban 过滤与 blocklist 的协作机制

Rack::Attack 中间件在处理请求时会依次执行 safelist、blocklist、throttle 和 track 规则。blocklist 块如果返回真值,请求会被立即拒绝,默认响应状态码是 403。这种短路特性非常适合做 IP 黑名单,因为被封禁地址不需要继续进入业务控制器,节省大量资源。

Fail2Ban.filter 方法承担了计数和封禁状态判断的工作。它接收一个缓存 key 以及三个时间相关参数:findtime 是统计失败次数的时间窗口,maxretry 是该窗口内允许的最大失败次数,bantime 是达到阈值后封禁的时长。当同一 IP 在 findtime 内失败次数达到 maxretry 时,filter 会在缓存中写入 banned 标记并返回 true,后续请求再进入 blocklist 时直接命中封禁状态,不会再执行业务代码。

因此,必须给 Rack::Attack 配置一个可用的缓存存储。Rails 中可以使用 ActiveSupport::Cache::MemoryStore 快速验证,但多进程或多实例部署时需要切换为 Redis、Memcached 等共享存储,否则每个进程的计数器互相独立,封禁规则会失去意义。

基础配置:初始化器与可疑路径过滤

先从一个最实用的场景入手:封禁那些频繁扫描敏感路径的 IP。正常用户不会在短时间内大量访问 .env、phpmyadmin、wp-admin 这类路径,所以可以直接在 blocklist 内使用请求特征作为失败条件。

以下初始化器写入 config/initializers/rack_attack.rb,配置了内存缓存和一条诱捕规则。filter 块返回 true 时记录一次失败,10 分钟内超过 3 次就封禁 2 小时。

# config/initializers/rack_attack.rb
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new

Rack::Attack.blocklist('fail2ban suspicious paths') do |req|
  Rack::Attack::Fail2Ban.filter("suspicious-paths-#{req.ip}", maxretry: 3, findtime: 10.minutes, bantime: 2.hours) do
    sensitive_path = ['/wp-admin', '/.env', '/phpmyadmin', '/actuator']
    sensitive_path.any? { |path| req.path.start_with?(path) }
  end
end

这个规则之所以有效,是因为它不依赖登录是否成功,只要请求目标本身足够可疑就应该被计数。注意缓存 key 中必须包含 IP,可以通过字符串插值生成,例如这里的 suspicious-paths-192.168.1.10。不要把 key 写成固定值,否则所有请求会共用一个计数器和封禁状态,导致一个攻击源触发后所有用户被误伤。

另外,Rack::Attack 的 blocklist 名称 fail2ban suspicious paths 只用于日志和调试,不会影响缓存 key。需要修改名称时不会影响已有封禁状态,但修改 key 前缀则会让旧计数失效。

登录失败计数:让 Fail2Ban 真正感知认证结果

上一节的规则针对路径特征,适合拦截扫描器。如果要对登录失败进行封禁,直接在 blocklist 中判断 req.path 和请求方法会带来问题:正常用户输错密码也会被计数,多次错误后可能被封禁。为了减少误伤,应当只在应用确认认证失败时调用 Fail2Ban.filter 来记录一次失败。

下面是在 SessionsController 中记录失败的代码。authenticate 方法返回 false 或 nil 时执行 filter,传 true 作为失败信号。maxretry、findtime 和 bantime 必须与 blocklist 端规则保持一致。

class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_path
    else
      Rack::Attack::Fail2Ban.filter("login-failures-#{request.ip}", maxretry: 5, findtime: 10.minutes, bantime: 1.hour) do
        true
      end
      render :new, status: :unprocessable_entity
    end
  end
end

这里需要理解封禁生效的时机。控制器中调用 filter 会把当前 IP 的失败计数加一,如果达到阈值则写入 banned 状态,但当前请求已经进入控制器,无法再被 Rack::Attack 拦截。被封禁的 IP 会从下一次请求开始触发 blocklist 规则。这是一种轻微延迟,但通常足够阻止自动化攻击继续尝试。

为了让后续请求真正被拦截,必须在 blocklist 中配置同一条 key。下面这段规则放在初始化器中,当请求携带的 IP 已经处于 banned 状态时,filter 会直接返回 true,不需要再运行内部块。

Rack::Attack.blocklist('fail2ban login failures') do |req|
  Rack::Attack::Fail2Ban.filter("login-failures-#{req.ip}", maxretry: 5, findtime: 10.minutes, bantime: 1.hour) do
    false
  end
end

内部块返回 false 表示不额外增加计数,因为计数只在控制器认证失败时进行。这样 blocklist 端只负责读取已封禁状态,不会因为普通访问登录页而误加次数。

多规则拆分与误封防御

实际项目中不应该把所有失败场景混在一个缓存 key 中。管理后台登录、API 令牌认证、敏感路径扫描的攻击成本和业务影响不同,应当拆分为多个 blocklist 规则,并设置不同的阈值和封禁时长。例如 API 认证失败可以设置 maxretry 为 10,findtime 为 5 分钟,bantime 为 30 分钟;管理后台登录失败可以设置 maxretry 为 5,封禁 1 小时。

缓存 key 的命名建议包含规则用途和 IP,例如 admin-login-failures-203.0.113.5。这样运维人员可以通过缓存客户端快速定位某个 IP 在哪条规则下被封禁。不要使用 IP 作为 key 的唯一组成部分,否则不同规则之间会共享计数,一条规则触发封禁后可能影响其它规则的判断。

为降低误封,可以配置 safelist 放行可信地址。比如公司内网、办公 VPN 出口、健康检查用户代理等。safelist 返回 true 时请求会直接跳过后续所有 blocklist 和 throttle 规则。

Rack::Attack.safelist('allow internal network') do |req|
  req.ip == '203.0.113.10' || req.ip.start_with?('10.0.')
end

如果希望给被封禁用户更友好的响应,可以自定义 blocklisted_response,返回纯文本或 JSON,避免默认的 403 页面让用户误以为服务故障。

Rack::Attack.blocklisted_response = lambda do |env|
  [403, {'Content-Type' => 'text/plain'}, ['您的请求过于频繁,IP 已被临时封禁']]
end

日常验证中,可以在 Rails console 里读取对应缓存 key,查看当前失败次数和过期时间。使用内存缓存时直接读取即可;使用 Redis 时可以通过 redis-cli 或 Rails.cache.read 检查。手动解封只需删除该 key,但要确认 key 名的完整拼写和 IP 格式。

生产环境缓存与中间件顺序

前面示例使用的 MemoryStore 只适合开发环境和单进程测试。如果应用通过 Puma 多进程或多容器部署,每个进程都会维护一份独立的内存计数,攻击者只需把请求分散到不同进程就能绕过封禁。生产环境应当换成共享缓存。以 Redis 为例,配合 redis-store 或 Rails 自带的 RedisCacheStore 配置 Rack::Attack。

Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(
  url: ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379/0'),
  expires_in: 2.hours
)

共享缓存还有另一个好处:所有实例看到同一份封禁列表,解封和排查也更简单。不过 Redis 故障时 Rack::Attack 可能无法计数,建议在缓存层配置超时和降级策略,避免安全模块拖垮整个请求链路。

中间件顺序也需要关注。Rack::Attack 默认插入 Rack 栈的较外层,如果希望依赖 Warden 或 Devise 的认证结果来判定失败,需要把 Rack::Attack 移到这些中间件之后。但在很多场景下,通过在控制器中调用 filter 记录失败可以绕开中间件顺序问题,因为此时认证过程已经完成,无需让 Rack::Attack 提前读取认证状态。采用这种方式后,中间件顺序保持默认即可,不需要对 Rails 默认栈做额外调整。

最后还要根据业务容忍度调整参数。findtime 太短会让慢速暴力破解逃过计数,太长又可能把几天前的失败累积到今天。bantime 太短起不到威慑作用,太长容易积累误封。建议先观察正常登录失败分布,再去设置阈值,并保留日志和手动解封通道。

Rack AttackFail2BanIP封禁修改时间:2026-10-03 01:35:24

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1003/64900.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。