Nginx + ModSecurity + OWASP CRS + Lua + GEOIP2 构建传统WAF
- 一、环境介绍
- 二、编译工具安装
- 三、编译安装ModSecurity v3
- 四、ModSecurity-Nginx 连接器下载
- 五、编译安装LuaJIT
- 六、ngx_devel_kit下载
- 七、lua-nginx-module下载
- 八、编译安装lua-resty-core
- 九、编译安装lua-resty-lrucache
- 十、OWASP Core Rule Set (CRS)下载
- 十一、下载geoip2模块和数据库
- 十二、编译libmaxminddb
- 十三、编译nginx
- 十四、集成部署
-
- 1、主要目录
- 2、ModSecurity V3配置文件modsecurity.conf配置
- 3、owasp crs 配置文件crs-setup.conf配置
- 4、ModSecurity V3+OWASP CRS集成配置文件main.conf(modsec目录)
- 5、开启modsecurity 模块配置文件modsec.conf(modsec目录),这里统一写,在server 中引用就可开启
- 6、误报白名单 whitelist.conf(modsec目录)
- 7、lua和geoip2(可选)
- 8、nginx主配置文件nginx.conf
- 9、默认配置文件default.conf
- 10、vhost 配置文件样例
- 11、测试
- 十五、日志分析工具建议
- 十六、后话
一、环境介绍
操作系统:龙蜥 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