Nginx + ModSecurity + OWASP CRS + Lua + GEOIP2 构建传统WAF

发布于:2025-07-05 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、环境介绍

操作系统:龙蜥 OS 8.9
服务器:nginx:10.99.99.99
nginx: 1.25.5
https://github.com/nginx/nginx/releases/tag/release-1.25.5
LuaJIT:v2.1-20250529
https://github.com/openresty/luajit2
ngx_devel_kit: 0.3.4
https://github.com/vision5/ngx_devel_kit
lua-nginx-module: 0.10.28
https://github.com/openresty/lua-nginx-module
lua-resty-core:v0.1.31
https://github.com/openresty/lua-resty-core
lua-resty-lrucache:v0.15
https://github.com/openresty/lua-resty-lrucache
ModSecurity v3(libmodsecurity):3.0.14
https://github.com/owasp-modsecurity/ModSecurity
ModSecurity-Nginx 连接器:1.0.4
https://github.com/owasp-modsecurity/ModSecurity-nginx/
OWASP Core Rule Set (CRS):4.15.0
https://github.com/coreruleset/coreruleset
geoip2:3.4
https://github.com/leev/ngx_http_geoip2_module
libmaxminddb:1.12.2
https://github.com/maxmind/libmaxminddb
GeoLite2 数据库
官方(需要注册):https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/
github分享:https://github.com/P3TERX/GeoLite.mmdb?tab=readme-ov-file

二、编译工具安装

dnf install epel-release -y
dnf install gcc gcc-c++ pcre pcre-devel zlib zlib-devel openssl openssl-devel make GeoIP-devel flex bison yajl yajl-devel curl-devel curl doxygen
dnf install -y autoconf automake libtool m4 git

可能会用到代理

git config --global http.proxy socks5h://x:10808
git config --global https.proxy socks5h://x:10808

三、编译安装ModSecurity v3

git clone --recursive https://github.com/owasp-modsecurity/ModSecurity ModSecurity
cd ModSecurity
git submodule update --init --recursive
./build.sh
./configure
make -j$(nproc)
make install

在这里插入图片描述
添加环境变量

tee /etc/profile.d/modsecurity.sh >/dev/null <<EOF
export PKG_CONFIG_PATH=/usr/local/modsecurity/lib/pkgconfig:$PKG_CONFIG_PATH
EOF

加载环境变量

source /etc/profile.d/modsecurity.sh

验证

pkg-config --modversion modsecurity

在这里插入图片描述

四、ModSecurity-Nginx 连接器下载

git clone https://github.com/owasp-modsecurity/ModSecurity-nginx.git

五、编译安装LuaJIT

git clone https://github.com/openresty/luajit2.git
cd luajit2/
make -j$(nproc)
make install PREFIX=/usr/local/luajit

添加环境变量

tee /etc/profile.d/luajit.sh >/dev/null <<EOF
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1
export LD_LIBRARY_PATH=/usr/local/luajit/lib:\$LD_LIBRARY_PATH
EOF

加载环境变量

source /etc/profile.d/luajit.sh

创建软连接

ln -sf /usr/local/luajit/bin/luajit /usr/local/bin/luajit

在这里插入图片描述

六、ngx_devel_kit下载

git clone https://github.com/vision5/ngx_devel_kit.git

七、lua-nginx-module下载

git clone https://github.com/openresty/lua-nginx-module.git

八、编译安装lua-resty-core

git clone https://github.com/openresty/lua-resty-core.git
cd lua-resty-core
make install

在这里插入图片描述

九、编译安装lua-resty-lrucache

git clone https://github.com/openresty/lua-resty-lrucache.git
cd lua-resty-lrucache
make install

十、OWASP Core Rule Set (CRS)下载

git clone https://github.com/coreruleset/coreruleset.git

十一、下载geoip2模块和数据库

git clone https://github.com/leev/ngx_http_geoip2_module.git

登陆后下载
https://www.maxmind.com/en/home
在这里插入图片描述

在这里插入图片描述
数据文件是这个三个
在这里插入图片描述

十二、编译libmaxminddb

读取 geoip2 数据库用

./configure
make
make check
make install
ldconfig

创建环境变量

tee /etc/profile.d/maxmind.sh >/dev/null <<EOF
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
EOF

加载环境变量

source /etc/profile.d/maxmind.sh

验证

pkg-config --modversion libmaxminddb

在这里插入图片描述

十三、编译nginx

git clone https://github.com/nginx/nginx.git -b release-1.25.5
./auto/configure \
  --prefix=/usr/local/nginx \
  --with-threads \
  --with-http_ssl_module \
  --with-http_v2_module \
  --with-http_realip_module \
  --with-http_gunzip_module \
  --with-http_gzip_static_module \
  --with-http_stub_status_module \
  --with-http_sub_module \
  --with-stream \
  --with-stream_ssl_module \
  --with-stream_realip_module \
  --with-stream_ssl_preread_module \
  --add-dynamic-module=/usr/local/src/ModSecurity-nginx \
  --add-module=/usr/local/src/ngx_devel_kit \
  --add-module=/usr/local/src/lua-nginx-module \
  --add-dynamic-module=/usr/local/src/ngx_http_geoip2_module
make
make install

十四、集成部署

注意:免费证书可以参考此文章申请

1、主要目录

# 虚拟主机配置文件目录,就是server
mkdir /usr/local/nginx/conf/vhost
# stream 流配置文件目录
mkdir /usr/local/nginx/conf/stream
# modsec crs 等安全模块目录
mkdir /usr/local/nginx/conf/modsec

2、ModSecurity V3配置文件modsecurity.conf配置

# 修改SecRuleEngine为On
SecRuleEngine On
# 日志文件位置
SecAuditLogFormat JSON
SecAuditLogType Serial
SecAuditLog /var/log/modsec_audit.json
# 正则匹配深度
SecPcreMatchLimit 100000
SecPcreMatchLimitRecursion 100000

3、owasp crs 配置文件crs-setup.conf配置

# 注释这两行
# SecDefaultAction "phase:1,log,auditlog,pass"
# SecDefaultAction "phase:2,log,auditlog,pass"
# 打开这两行注释,nolog不会记录日志到nginx日志里
# 日志记录位置为modsec_audit.log,根据评分拦截,修改如下
SecDefaultAction "phase:1,nolog,auditlog,pass"
SecDefaultAction "phase:2,nolog,auditlog,pass"

4、ModSecurity V3+OWASP CRS集成配置文件main.conf(modsec目录)

# ModSecurity V3 配置文件
Include /usr/local/src/ModSecurity/modsecurity.conf
# 误报白名单设置
Include /usr/local/nginx/conf/modsec/whitelist.conf
# OWASP CRS 初始化设置
Include /usr/local/src/coreruleset/crs-setup.conf
# OWASP 提供的具体防护规则
Include /usr/local/src/coreruleset/rules/*.conf

5、开启modsecurity 模块配置文件modsec.conf(modsec目录),这里统一写,在server 中引用就可开启

# 开启WAF
modsecurity on;
# 加载WAF规则文件
modsecurity_rules_file /usr/local/nginx/conf/modsec/main.conf;

6、误报白名单 whitelist.conf(modsec目录)

# 这只是示例 根据审计日志/var/log/modsec_audit.log情况写
SecRule REQUEST_URI "@beginsWith /zabbix/jsrpc.php" "id:1000001,phase:1,nolog,pass,ctl:ruleRemoveById=920420"

7、lua和geoip2(可选)

注:lua可以做一些对modsec检测前或者检测后的 功能增强,也可以结geoip2 做限制访问

(1)lua cookie 转发脚本,结合hash 做基于cookie 的转发

local ck = require "resty.cookie"
local cookie, err = ck:new()
if not cookie then
    ngx.log(ngx.ERR, "failed to instantiate cookie: ", err)
    return ngx.exit(500)
end

-- 尝试获取名为 route_key 的 cookie
local route_key, err = cookie:get("route_key")
if not route_key then
    -- 生成唯一值(优先用 ngx.var.request_id,否则用更安全的随机数)
    local new_route
    if ngx.var.request_id then
        new_route = ngx.var.request_id
    else
        -- 更安全的随机数(非加密安全,但比 math.random 好)
        new_route = ngx.time() .. ngx.md5(ngx.var.connection .. math.random())
    end

    -- 设置 cookie,客户端会收到这个 cookie
    local ok, err = cookie:set({
        key = "route_key",
        value = new_route,
        path = "/",
        httponly = true,
        max_age = 3600,  -- 1小时(单位:秒)
    })
    if not ok then
        ngx.log(ngx.ERR, "failed to set cookie 'route_key': ", err)
        -- 可以选择继续执行(即使 Cookie 设置失败),或返回错误
        -- return ngx.exit(500)
    end

    -- 把生成的新值赋给 nginx 变量供 hash 使用
    ngx.var.route_key = new_route
else
    -- 已有 cookie,赋值给 nginx 变量
    ngx.var.route_key = route_key
end

(2)使用geoip2 做地区性l拦截

vi /usr/local/nginx/conf/modsec/geoip_rules.conf
SecGeoLookupDb /usr/local/src/geoip2-db/GeoLite2-Country.mmdb

SecRule GEO:COUNTRY_CODE "@streq CN" \
    "id:1001,phase:1,deny,status:403,log,msg:'Blocked access from China'"

在 main.conf 中添加规则引用

#加载 GeoIP2 国家限制规则
Include /usr/local/nginx/conf/modsec/geoip_rules.conf

8、nginx主配置文件nginx.conf

# 加载模块写在最顶端
load_module /usr/local/nginx/modules/ngx_http_geoip2_module.so;
load_module /usr/local/nginx/modules/ngx_http_modsecurity_module.so;
load_module /usr/local/nginx/modules/ngx_stream_geoip2_module.so;
# 普通用户启动 (useradd nginx -s /sbin/nologin -M)
user nginx;

# 配置nginx worker进程个数
#worker_processes 8;
#worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 #01000000 10000000;
#worker_cpu_affinity 0001 0010 0100 1000 0001 0010 1000 0001 0010 0100 1000;

worker_processes 4;
worker_cpu_affinity 0001 0010 0100 1000;

#worker_processes 2;
#worker_cpu_affinity 0101 1010;

# 配置日志存放路径
error_log   logs/error.log error;
pid logs/nginx.pid;

# nginx事件处理模型优化
events {
    worker_connections 51200; # 当个进程允许的客户端最大连接数
    use epoll;
    }

# 配置nginx worker进程最大打开文件数
worker_rlimit_nofile 65535;

http {
    # lua配置
    lua_package_path "/usr/local/lib/lua/?.lua;/usr/local/lib/lua/?/init.lua;;";
    lua_package_cpath "/usr/local/lib/lua/?.so;;";
    
    init_by_lua_block {
            require "resty.core"
            collectgarbage("collect")
        }

    # 加载GeoiIP2 数据库
    geoip2 /usr/local/src/geoip2-db/GeoLite2-City.mmdb {
        auto_reload 5m;
        $geoip2_data_country_code country iso_code;
        $geoip2_data_city_name city names en;
        $geoip2_data_subdivision subdivision 0 names en;
        $geoip2_data_latitude location latitude;
        $geoip2_data_longitude location longitude;
    }

    # 隐藏版本号
    server_tokens off;
    
    # 设置日志格式
    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  logs/accesslog.log  main;        
    # 开启高效文件传输模式
    include mime.types;                         # 媒体类型
    default_type    application/octet-stream;   # 默认媒体类型
    charset  utf-8;                             # 默认字符集
    sendfile    on;
    tcp_nopush  on;                             # 只有在sendfile开启模式下有效
    
    # 设置连接超时时间
    keepalive_timeout  65;      # 设置客户端连接保持会话的超时时间,超过则服务器会关闭该连接
    tcp_nodelay on;             # 打开tcp_nodelay,在包含了keepalive参数才有效果
    client_header_timeout 15;    # 设置客户端请求有超时时间,该时间内客户端未发送数据,nginx将返回‘Request time out(408)’错误
    client_body_timeout 15;    # 设置客户端请求体超时时间,同上
    send_timeout 15;            # 设置相应客户端的超时时间,超时nginx将会关闭连接
    
    # 上传文件大小设置(动态引用)
    client_max_body_size 10m;
    
    # 数据包头部缓存大小
    client_header_buffer_size    1k;        #默认请求包头信息的缓存    
    large_client_header_buffers  4 4k;      #大请求包头部信息的缓存个数与容量
   
    # 压缩处理
    gzip on;                           #开启压缩
    gzip_min_length 1k;                #小文件不压缩
    gzip_comp_level 4;                 #压缩比率
    gzip_buffers 4 16k;                #压缩缓冲区大小,申请4个单位为16K的内存作为亚索结果流缓存    
    gzip_http_version 1.1;             # 默认压缩版本
    #对特定文件压缩,类型参考mime.types
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_vary on;
    gzip_disable "MSIE[1-6]\."; 
    
    # 设置fastcgi
    fastcgi_cache_path /usr/local/nginx/fastcgi_cache levels=1:2
                keys_zone=TEST:10m
                inactive=5m;       # 为FastCGI缓存指定一个路径,目录结构等级,关键字区域存储时间和非活动删除时间
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    fastcgi_connect_timeout 300;   # 指定连接到后端FastCGI的超时时间
    fastcgi_send_timeout 300;      # 向FastCGI传送请求的超时时间,这个值是指已经完成两次握手后向FastCGI传送请求的超时时间   
    fastcgi_read_timeout 300;      # 接收FastCGI应答的超时时间,这个值是指已经完成两次握手后接收FastCGI应答的超时时间    
    fastcgi_buffer_size 16k;       # 缓冲区大小 
    fastcgi_buffers 16 16k;
    fastcgi_busy_buffers_size 16k;      
    fastcgi_temp_file_write_size 16k;   # 在写入fastcgi_temp_path时将用多大的数据块,默认值是fastcgi_buffers的两倍  
    fastcgi_cache TEST;                 # 开启FastCGI缓存并且为其制定一个名称
    fastcgi_cache_valid 200 302 1h;
    fastcgi_cache_valid 301 1d;
    fastcgi_cache_valid any 1m;         # 为指定的应答代码指定缓存时间,上例中将200,302应答缓存一小时,301应答缓存1天,其他为1分钟
    fastcgi_cache_min_uses 1;           # 5分钟内某文件1次也没有被使用,那么这个文件将被移除
    fastcgi_cache_use_stale error timeout invalid_header http_500;
    
    # 内存缓存
    open_file_cache   max=2000  inactive=20s; #设置服务器最大缓存文件数量,关闭20秒内无请求的文件
    open_file_cache_valid    60s;             #文件句柄的有效时间是60秒,60秒后过期     
    open_file_cache_min_uses 5;               #只有访问次数超过5次会被缓存  
    open_file_cache_errors   off;

   # 引入子配置文件     
    include vhost/*.conf;

}


#配置tcp代理,需要额外加载stream相关模块
stream {
#
#    upstream cloudsocket {
#       hash $remote_addr consistent;
#      # $binary_remote_addr;
#      server 192.168.182.155:3306 weight=5 max_fails=3 fail_timeout=30s;
#    }
#    server {
#       listen 3306;#数据库服务器监听端口
#       proxy_connect_timeout 10s;
#       proxy_timeout 300s;#设置客户端和代理服务之间的超时时间,如果5分钟内没操作将自动断开。
#       proxy_pass cloudsocket;
#    }
   # 引入子配置文件     
    include stream/*.conf;
}

9、默认配置文件default.conf

server {
  listen 80 default_server;
  server_name _; # 匹配所有域名
  include /usr/local/nginx/conf/modsec/modsec.conf;

  location / {
    # 返回 403 错误,禁止 IP 地址访问
    return 403;
  }
}

server {
    listen 443 ssl default_server;
    server_name _;
    include /usr/local/nginx/conf/modsec/modsec.conf;

    ssl_certificate    /usr/local/nginx/conf/cert/gubeisz.net/fullchain.cer;
    ssl_certificate_key      /usr/local/nginx/conf/cert/gubeisz.net/gubeisz.net.key;

    # 其他 SSL 配置

    location / {
        return 403;
    }
}

10、vhost 配置文件样例

upstream zabbix{

server 10.99.50.110:80 weight=1 max_fails=2 fail_timeout=10;
#server 192.168.2.101 down;#标记为down 剔除负载均衡队列
}


server
{
    listen 80;
    server_name zabbix.xxx.net;
    include /usr/local/nginx/conf/modsec/modsec.conf;
    #access_log logs/example-access.log main;
    #error_log logs/example-error.log  error;
    proxy_buffering on; #开启buffer缓存,异步应答客户端请求,效率高
    proxy_buffer_size 4k;
    proxy_buffers 2 4k;
    proxy_busy_buffers_size 4k;
    #proxy_temp_path /tmp/nginx_proxy_tmp 1 2;
    proxy_max_temp_file_size 20M;
    proxy_temp_file_write_size 8k;
    
    location /
    {
      
        proxy_pass      http://zabbix;
        proxy_method $request_method;
        proxy_set_header Host   $host;
        proxy_set_header X-Real-IP      $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

server {
    listen 443 ssl;
    server_name  zabbix.xxx.net;
    include /usr/local/nginx/conf/modsec/modsec.conf;
    #access_log logs/example-access.log main;
    #error_log logs/example-error.log  error;
    ssl_certificate    /usr/local/nginx/conf/cert/xxx.net/fullchain.cer;
    ssl_certificate_key      /usr/local/nginx/conf/cert/xxx.net/xxx.net.key;
    ssl_session_timeout 5m;
    ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
    #ssl_ciphers   ALL:!DH:!EXPORT:!RC4:+HIGH:+MEDIUM:-LOW:!aNULL:!eNULL;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!3DES:!ADH:!RC4:!DH:!DHE;
    ssl_prefer_server_ciphers  on;

    location /
    {

        proxy_pass      http://zabbix;
        proxy_method $request_method;
        proxy_set_header Host   $host;
        proxy_set_header X-Real-IP      $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

11、测试

sql注入

curl 'http://zabbix.xxx.net/zabbix?id=1%20or%201=1'

在这里插入图片描述
显示403,被拦截了,日志显示如下
在这里插入图片描述

十五、日志分析工具建议

ModSecurity Audit Log + ELK Stack (Elasticsearch + Logstash + Kibana)

十六、后话

我在测试部署的过程中,发现最大的问题就是误报;升高crs的评分如下,会导致真正的攻击也不被拦截,做法是使用modsec 观察模式+ crs评分机制,找到误报,手动加白名单,再开启modsec 拦截模式

#SecAction \
#    "id:900110,\
#    phase:1,\
#    pass,\
#    t:none,\
#    nolog,\
#    tag:'OWASP_CRS',\
#    ver:'OWASP_CRS/4.16.0-dev',\
#    setvar:tx.inbound_anomaly_score_threshold=5,\
#    setvar:tx.outbound_anomaly_score_threshold=4"

手动处理加白名单比较痛苦,谁也没时间整天盯着这个玩意,因此转战下一个测试
Coraza + KoaLA


网站公告

今日签到

点亮在社区的每一天
去签到