使用 OpenResty 和 View 构建高性能 WAF:全面解析与实战

目录

  1. OpenResty 与 WAF 的契合点
  2. OpenResty 的 Lua 处理阶段概览
  3. init_by_lua*:全局初始化
  4. init_worker_by_lua*:Worker 进程初始化
  5. ssl_certificate_by_lua*:动态 SSL 证书处理
  6. set_by_lua*:设置 Nginx 变量
  7. rewrite_by_lua*:URL 重写与重定向检查
  8. access_by_lua*:访问控制与认证
  9. content_by_lua*:内容生成与动态防护
  10. header_filter_by_lua*:响应头处理
  11. body_filter_by_lua*:响应体过滤
  12. log_by_lua*:日志记录与分析
  13. balancer_by_lua*:负载均衡安全策略
  14. stream_by_lua*:非 HTTP 流防护
  15. timer_by_lua*:定时任务防护
  16. 部署与优化建议
  17. 总结

OpenResty 与 WAF 的契合点

OpenResty 的核心优势在于其非阻塞、事件驱动的架构,结合 LuaJIT 的高性能脚本执行能力,能够在高并发场景下高效处理请求。对于 WAF 来说,这种架构意味着可以在每个请求的处理阶段插入安全检查逻辑,而不会显著增加延迟。此外,OpenResty 提供了丰富的 Lua API(如 ngx.varngx.reqngx.resp),可以轻松访问和修改请求/响应的各种属性,非常适合实现复杂的防护规则。

为什么选择 OpenResty 构建 WAF?

  • 高性能:基于 Nginx 的事件驱动模型,单进程可处理数万并发请求。
  • 灵活性:Lua 脚本允许动态配置规则,无需重启服务。
  • 轻量级:LuaJIT 占用内存少,执行速度快。
  • 模块化:支持 Redis、Memcached 等外部存储,适合分布式 WAF 部署。
  • 社区支持:OpenResty 拥有活跃的社区和丰富的 Lua 模块(如 lua-resty-waf)。

接下来,我们将逐一解析 OpenResty 的 Lua 处理阶段,探讨如何在每个阶段实现 WAF 功能。

OpenResty 的 Lua 处理阶段概览

OpenResty 将 HTTP 请求处理分为多个阶段,每个阶段都有特定的作用。通过在这些阶段注入 Lua 脚本,我们可以实现从初始化到日志记录的全面安全防护。以下是 HTTP 请求的阶段顺序(不包括非请求阶段):

  1. ssl_certificate_by_lua*(若启用 SSL/TLS)
  2. set_by_lua*
  3. rewrite_by_lua*
  4. access_by_lua*
  5. content_by_lua*
  6. header_filter_by_lua*
  7. body_filter_by_lua*
  8. log_by_lua*
  9. balancer_by_lua*

此外,还有非请求阶段(如 init_by_lua*init_worker_by_lua*timer_by_lua*)和流模块阶段(如 stream_by_lua*)。我们将逐一讲解每个阶段的 WAF 应用。

init_by_lua*:全局初始化

作用与原理

init_by_lua* 在 Nginx 主进程启动时执行,仅运行一次,用于全局配置的初始化。WAF 可以在此阶段加载黑白名单、规则库或连接外部存储(如 Redis)。

  • 适用场景:初始化全局共享数据(如 IP 黑名单、恶意 User-Agent 列表)。
  • 注意事项:此阶段运行在主进程,需避免阻塞操作。

示例:加载 IP 黑名单

假设我们维护一个 IP 黑名单文件 /etc/nginx/blacklist.txt,在 init_by_lua* 中加载到全局 Lua 表中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
init_by_lua_block {
    -- 创建全局黑名单表
    _G.blacklist = {}
    
    -- 读取黑名单文件
    local file = io.open("/etc/nginx/blacklist.txt", "r")
    if file then
        for line in file:lines() do
            -- 去除换行符并存入表
            line = line:gsub("%s+$", "")
            if line ~= "" then
                _G.blacklist[line] = true
            end
        end
        file:close()
    end
}

代码说明

  • 使用 _G 创建全局表 blacklist,存储黑名单 IP。
  • 读取 /etc/nginx/blacklist.txt,每行一个 IP 地址。
  • 后续阶段可通过 _G.blacklist[ip] 检查 IP 是否在黑名单中。

init_worker_by_lua*:Worker 进程初始化

作用与原理

init_worker_by_lua* 在每个 Nginx Worker 进程启动时执行,用于 Worker 级别的初始化。WAF 可以在此阶段为每个 Worker 初始化本地缓存或计数器。

  • 适用场景:初始化 Worker 级别的限流计数器或临时黑名单。
  • 注意事项:每个 Worker 独立运行,数据不共享。

示例:初始化请求限流计数器

为每个 Worker 初始化一个简单的请求计数器,用于后续限流。

1
2
3
4
init_worker_by_lua_block {
    -- Worker 级别的请求计数器
    ngx.shared.counter = ngx.shared.counter or ngx.shared:new("counter", 1024*1024)
}

代码说明

  • 使用 ngx.shared 创建共享内存字典 counter,在 Worker 间共享。
  • 后续可在 access_by_lua* 中使用此计数器实现限流。

ssl_certificate_by_lua*:动态 SSL 证书处理

作用与原理

ssl_certificate_by_lua* 在 SSL/TLS 握手阶段执行,用于动态选择或配置 SSL 证书。WAF 可以在此阶段检查客户端证书或 TLS 扩展(如 SNI)。

  • 适用场景:检测可疑的 TLS 客户端证书或 SNI 伪装。
  • 注意事项:需启用 SSL 模块,且此阶段对性能敏感。

示例:检查 SNI 合法性

检查客户端请求的 SNI 是否在允许的域名列表中。

1
2
3
4
5
6
7
8
9
ssl_certificate_by_lua_block {
    local sni = ngx.ssl.server_name()
    local allowed_sni = {["example.com"] = true, ["www.example.com"] = true}
    
    if not sni or not allowed_sni[sni] then
        ngx.log(ngx.ERR, "Invalid SNI: ", sni or "none")
        return ngx.exit(ngx.ERROR)
    end
}

代码说明

  • 使用 ngx.ssl.server_name() 获取 SNI。
  • 检查 SNI 是否在白名单中,否则终止连接。

set_by_lua*:设置 Nginx 变量

作用与原理

set_by_lua* 用于设置 Nginx 变量,常用于请求处理早期设置标志或参数。WAF 可以在此阶段为请求标记风险等级。

  • 适用场景:为请求设置安全评分或标记。
  • 注意事项:适合轻量逻辑,避免复杂计算。

示例:标记高风险 User-Agent

为包含特定 User-Agent 的请求设置高风险标志。

1
2
3
4
5
6
7
8
set_by_lua_block {
    local ua = ngx.var.http_user_agent or ""
    if ua:match("bot") or ua:match("spider") then
        ngx.var.risk_level = "high"
    else
        ngx.var.risk_level = "low"
    end
}

代码说明

  • 检查 http_user_agent 是否包含 “bot” 或 “spider”。
  • 设置 ngx.var.risk_level 变量,供后续阶段使用。

rewrite_by_lua*:URL 重写与重定向检查

作用与原理

rewrite_by_lua* 在 URL 重写阶段执行,用于修改请求 URI 或执行重定向。WAF 可以在此阶段检测恶意的 URI 模式(如 SQL 注入)。

  • 适用场景:阻止恶意 URI 或重定向可疑请求。
  • 注意事项:可触发内部重定向,需避免循环。

示例:检测 SQL 注入模式

检查 URI 是否包含 SQL 注入关键字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
rewrite_by_lua_block {
    local uri = ngx.var.uri
    local sql_injection_patterns = {
        "union%s+select",
        "drop%s+table",
        "or%s+1=1"
    }
    
    for _, pattern in ipairs(sql_injection_patterns) do
        if uri:match(pattern) then
            ngx.log(ngx.WARN, "SQL injection detected: ", uri)
            return ngx.redirect("/403.html", ngx.HTTP_FORBIDDEN)
        end
    end
}

代码说明

  • 检查 URI 是否匹配 SQL 注入模式(如 union select)。
  • 若检测到攻击,重定向到 403 页面。

access_by_lua*:访问控制与认证

作用与原理

access_by_lua* 在访问控制阶段执行,用于认证和授权检查。WAF 可以在此阶段实现 IP 黑名单检查、限流或令牌验证。

  • 适用场景:阻止黑名单 IP、限制请求速率。
  • 注意事项:常用于终止请求,需快速执行。

示例:IP 黑名单与限流

结合 init_by_lua* 的黑名单和 init_worker_by_lua* 的计数器,实现 IP 黑名单检查和限流。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
access_by_lua_block {
    local ip = ngx.var.remote_addr
    -- 检查黑名单
    if _G.blacklist[ip] then
        ngx.log(ngx.WARN, "Blocked IP: ", ip)
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end
    
    -- 限流:每分钟 100 次请求
    local key = ip .. ":rate"
    local counter = ngx.shared.counter
    local count = counter:get(key) or 0
    
    if count >= 100 then
        ngx.log(ngx.WARN, "Rate limit exceeded for IP: ", ip)
        return ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
    end
    
    counter:incr(key, 1, 60) -- 计数加1,过期时间60秒
}

代码说明

  • 检查 IP 是否在 _G.blacklist 中,若是则返回 403。
  • 使用共享内存实现每分钟 100 次请求的限流,超限返回 429。

content_by_lua*:内容生成与动态防护

作用与原理

content_by_lua* 在内容生成阶段执行,用于生成响应内容。WAF 可以在此阶段动态生成错误页面或检查 POST 请求体。

  • 适用场景:处理复杂请求体检查或自定义错误页面。
  • 注意事项:适合复杂逻辑,但需注意性能。

示例:检查 POST 请求体

检测 POST 请求体中的 XSS 攻击。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
content_by_lua_block {
    if ngx.req.get_method() == "POST" then
        ngx.req.read_body()
        local body = ngx.req.get_body_data()
        if body and body:match("<script") then
            ngx.log(ngx.WARN, "XSS detected in POST body")
            ngx.status = ngx.HTTP_FORBIDDEN
            ngx.say("XSS attack detected!")
            return
        end
    end
    -- 正常处理
    ngx.say("Request processed successfully")
}

代码说明

  • 检查 POST 请求体是否包含 <script 标签。
  • 若检测到 XSS,返回 403 并显示错误信息。

header_filter_by_lua*:响应头处理

作用与原理

header_filter_by_lua* 在响应头处理阶段执行,用于修改或添加响应头。WAF 可以在此阶段添加安全头(如 CSP、X-XSS-Protection)。

  • 适用场景:增强响应安全性,防止客户端攻击。
  • 注意事项:仅处理头信息,执行快速。

示例:添加安全响应头

为所有响应添加常见的安全头。

1
2
3
4
5
header_filter_by_lua_block {
    ngx.header["Content-Security-Policy"] = "default-src 'self'"
    ngx.header["X-XSS-Protection"] = "1; mode=block"
    ngx.header["X-Frame-Options"] = "DENY"
}

代码说明

  • 添加 CSP、XSS 防护和防点击劫持的响应头。
  • 提升客户端浏览器的安全性。

body_filter_by_lua*:响应体过滤

作用与原理

body_filter_by_lua* 在响应体处理阶段执行,用于修改或过滤响应内容。WAF 可以在此阶段替换敏感数据或检测泄露信息。

  • 适用场景:过滤响应中的敏感信息(如信用卡号)。
  • 注意事项:可能多次调用,需高效处理。

示例:过滤信用卡号

将响应体中的信用卡号替换为星号。

1
2
3
4
5
6
7
8
body_filter_by_lua_block {
    local data = ngx.arg[1]
    if data then
        -- 简单匹配 16 位信用卡号(示例)
        data = data:gsub("%d%d%d%d[-%s]?%d%d%d%d[-%s]?%d%d%d%d[-%s]?%d%d%d%d", "****-****-****-****")
        ngx.arg[1] = data
    end
}

代码说明

  • 使用正则表达式匹配 16 位信用卡号。
  • 将匹配内容替换为 ****-****-****-****

log_by_lua*:日志记录与分析

作用与原理

log_by_lua* 在日志记录阶段执行,用于自定义日志或发送警报。WAF 可以在此阶段记录攻击事件或发送到外部系统(如 ELK)。

  • 适用场景:记录可疑请求,生成安全报告。
  • 注意事项:异步执行,不影响响应。

示例:记录可疑请求

将高风险请求记录到单独日志文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
log_by_lua_block {
    if ngx.var.risk_level == "high" then
        local log_entry = string.format(
            '[%s] High risk request: IP=%s, URI=%s, UA=%s\n',
            os.date("%Y-%m-%d %H:%M:%S"),
            ngx.var.remote_addr,
            ngx.var.uri,
            ngx.var.http_user_agent or ""
        )
        local file = io.open("/var/log/nginx/waf.log", "a")
        if file then
            file:write(log_entry)
            file:close()
        end
    end
}

代码说明

  • 检查 risk_level 是否为 “high”。
  • 将请求信息写入 /var/log/nginx/waf.log

balancer_by_lua*:负载均衡安全策略

作用与原理

balancer_by_lua* 在上游负载均衡阶段执行,用于自定义后端服务器选择。WAF 可以在此阶段实现基于安全的负载均衡策略。

  • 适用场景:隔离可疑请求到特定后端。
  • 注意事项:需配置上游服务器。

示例:隔离高风险请求

将高风险请求路由到隔离服务器。

1
2
3
4
5
6
balancer_by_lua_block {
    if ngx.var.risk_level == "high" then
        local balancer = require "ngx.balancer"
        balancer.set_current_peer("127.0.0.1", 8081) -- 隔离服务器
    end
}

代码说明

  • 检查 risk_level,若为 “high”,路由到隔离服务器(127.0.0.1:8081)。
  • 需在 Nginx 配置中定义上游服务器。

stream_by_lua*:非 HTTP 流防护

作用与原理

stream_by_lua* 用于处理 TCP/UDP 流量,适用于非 HTTP 协议。WAF 可以在此阶段实现端口级别的防护(如阻止恶意 SSH 连接)。

  • 适用场景:保护数据库或邮件服务器。
  • 注意事项:需启用流模块。

示例:阻止黑名单 IP 的 TCP 连接

阻止黑名单 IP 访问数据库端口。

1
2
3
4
5
6
7
stream_by_lua_block {
    local ip = ngx.var.remote_addr
    if _G.blacklist[ip] then
        ngx.log(ngx.WARN, "Blocked TCP connection from IP: ", ip)
        return ngx.exit(ngx.ERROR)
    end
}

代码说明

  • 检查 TCP 连接的客户端 IP 是否在黑名单中。
  • 若在黑名单,终止连接。

timer_by_lua*:定时任务防护

作用与原理

timer_by_lua* 用于运行后台定时任务,WAF 可以在此阶段更新黑名单或清理缓存。

  • 适用场景:动态更新规则或清理过期数据。
  • 注意事项:异步执行,避免阻塞。

示例:定期更新黑名单

每 5 分钟更新黑名单。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
init_by_lua_block {
    local function update_blacklist()
        -- 重新加载黑名单
        _G.blacklist = {}
        local file = io.open("/etc/nginx/blacklist.txt", "r")
        if file then
            for line in file:lines() do
                line = line:gsub("%s+$", "")
                if line ~= "" then
                    _G.blacklist[line] = true
                end
            end
            file:close()
        end
        -- 5 分钟后再次运行
        ngx.timer.at(300, update_blacklist)
    end
    -- 启动定时任务
    ngx.timer.at(0, update_blacklist)
}

代码说明

  • 每 300 秒(5 分钟)重新加载黑名单。
  • 使用 ngx.timer.at 实现循环任务。

部署与优化建议

部署步骤

  1. 安装 OpenResty:从 openresty.org 下载并编译,支持 Lua 和流模块。
  2. 配置 Nginx:在 nginx.conf 中添加 Lua 指令,引用上述脚本。
  3. 测试规则:使用 curl 或压测工具(如 ab)验证 WAF 效果。
  4. 监控日志:检查 /var/log/nginx/waf.log 和 Nginx 错误日志。

优化技巧

  • 减少 I/O 操作:将黑名单缓存到 Redis 或共享内存。
  • 异步日志:使用 ngx.timer 异步写入日志,避免阻塞。
  • 规则优化:定期分析日志,精简匹配模式。
  • 分布式部署:结合 Redis 实现集群化 WAF。

总结

通过 OpenResty 和 Lua,我们可以在请求处理的每个阶段实现灵活、高效的 Web 安全防护。从全局初始化到日志记录,每个阶段都有独特的用途,适合不同的防护需求。本教程提供的示例代码简单易懂,适合初学者快速上手,同时也为进阶用户提供了实用的参考。

希望这篇教程能帮助你构建自己的高性能 WAF!如果有任何问题,欢迎在博客评论区留言,我们一起探讨!

评论 0