开源框架openresty+nginx 实现web应用防火墙(WAF)
1、简介
Web应用防火墙(Web Application Firewall, WAF),通过对HTTP(S)请求进行检测,识别并阻断SQL注入、跨站脚本攻击(Cross Site Scripting xss)、网页木马上传、命令/代码注入、文件包含、敏感文件访问、第三方应用漏洞攻击、CC(挑战黑洞)攻击、恶意爬虫扫描、跨站请求伪造等攻击,保护Web服务安全稳定。
本文主要是通过春哥的开源框架openresty来实现WAF;
参考:https://github.com/openresty
2、架构
整体架构:春哥的openresty开源框架(该框架集成了nginx、lua、lua-nginx-module等模块),推荐使用该框架;
如果想使用原生的nginx、lua来搭建waf,请参阅 nginx lua lua-nginx-module构建web应用防火墙(waf)
3、实现功能
支持IP白名单和黑名单功能,直接将黑名单的IP访问拒绝。
支持URL白名单,将不需要过滤的URL进行定义。
支持User-Agent的过滤,匹配自定义规则中的条目,然后进行处理(返回403)。
支持CC攻击防护,单个URL指定时间的访问次数,超过设定值,直接返回403。
支持Cookie过滤,匹配自定义规则中的条目,然后进行处理(返回403)。
支持URL过滤,匹配自定义规则中的条目,如果用户请求的URL包含这些,返回403。
支持URL参数过滤,原理同上。
支持日志记录,将所有拒绝的操作,记录到日志中去。
日志记录为JSON格式,便于日志分析,例如使用ELKStack进行攻击日志收集、存储、搜索和展示。
4、安装包下载
提供了各种版本openresty,点击链接,去下载适合版本。
安装openresty
创建安装包目录并将下载的安装包上传至该目录(也可以直接通过wget命令直接下载)
解压 tar zxvf openresty-1.15.8.1.tar.gz
cd openresty-1.15.8.1
安装基本包
yum install -y readline-devel pcre-devel openssl-devel gcc gcc-c++ perl.x86_64
配置环境
1 |
./configure --prefix=/usr/local/openresty --with-luajit --with-http_v2_module --with-http_stub_status_module --with-http_ssl_module --with-http_gzip_static_module --with-ipv6 --with-http_sub_module --with-pcre --with-pcre-jit --with-file-aio --with-http_dav_module |
编译 gmake
安装 gmake install
修改nginx.conf,测试是否安装成功
编辑nginx.conf
vi nginx.conf
1 2 3 4 5 6 7 8 |
server { location /hello { default_type text/html; content_by_lua_block { ngx.say("HelloWorld") } } } |
启动nginx 一次执行下面代码段
1 2 3 4 5 |
cd ../sbin ps -ef|grep nginx ./nginx -t ./nginx ps -ef|grep nginx |
关闭防火墙或者开放虚拟机防火墙端口,
这里为了方便,直接就关闭防火墙了,生产中必须采用开放防火墙端口的方式
systemctl stop firewalld
systemctl status firewalld
浏览器访问测试 http://10.10.91.23/hello
6、部署waf
下载waf包 https://github.com/unixhot/waf
可以使用wget,也可以下载到本地,让后上传到虚拟机
解压后,将waf复制到 /usr/local/openresty/nginx/conf/
cp -r waf /usr/local/openresty/nginx/conf/
修改Nginx的配置文件,加入以下配置。注意路径,同时WAF日志默认存放在/tmp/日期_waf.log
/usr/local/openresty/nginx/conf
vi nginx.conf
1 2 3 4 5 |
#WAF lua_shared_dict limit 50m; lua_package_path "/usr/local/openresty/nginx/conf/waf/?.lua"; init_by_lua_file "/usr/local/openresty/nginx/conf/waf/init.lua"; access_by_lua_file "/usr/local/openresty/nginx/conf/waf/access.lua"; |
修改waf中的配置信息
cd /waf
vi config.lua
重启nginx即可测试
加上redis 自动加入黑名单设置。
cp /usr/local/openresty/lualib/resty/redis.lua /usr/local/openresty/nginx/conf/waf/
如果报如下错就是reids.lua 没有读取到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
lua entry thread aborted: runtime error: /usr/local/openresty/nginx/conf/waf/redis.lua:1: module 'resty. redis' not found: no field package.preload['resty.redis'] no file '/usr/local/openresty/nginx/conf/waf/resty/redis.lua' no file '/usr/local/openresty/site/lualib/resty/redis.so' no file '/usr/local/openresty/lualib/resty/redis.so' no file './resty/redis.so' no file '/usr/local/lib/lua/5.1/resty/redis.so' no file '/usr/local/openresty/luajit/lib/lua/5.1/resty/redis.so' no file '/usr/local/lib/lua/5.1/loadall.so' no file '/usr/local/openresty/site/lualib/resty.so' no file '/usr/local/openresty/lualib/resty.so' no file './resty.so' no file '/usr/local/lib/lua/5.1/resty.so' no file '/usr/local/openresty/luajit/lib/lua/5.1/resty.so' no file '/usr/local/lib/lua/5.1/loadall.so' |
编写链接Redis的Lua脚本 /usr/local/openresty/nginx/conf/waf/redis.lua
cat redis.lua
1 2 3 4 5 6 7 8 9 |
local redis = require "resty.redis" local conn = redis.new() conn.connect(conn, '192.168.200.195', '6379') local res = conn:get("123") if res==ngx.null then ngx.say("redis集群中不存在KEY——'123'") return end ngx.say(res) |
在nginx配置文件中添加以下location
1 2 3 4 |
location /lua_redis { default_type text/plain; content_by_lua_file /usr/local/openresty/nginx/conf/waf/redis.lua; } |
4、验证
在浏览器输入ip/lua_redis
看能不能获取到刚才在redis添加的”123″ 这个key 并获取到他的值
如果能看到下图的内容表示可以访问redis
好的,准备工作已经准备好了,现在要来最终的Nginx+Lua+Redis自动封禁并解封IP了
三、Nginx+Lua+Redis
1、添加访问控制的Lua脚本
(此脚本需要改的只有下面的这一句,把redis的ip和端口替换一下即可
ok, err = conn:connect(“192.168.1.222”, 6379)
温馨提示:如果在nginx的上层有用到阿里云的SLB负载均衡的话需要修改一下脚本里的所有…ngx.var.remote_addr
把remote_addr替换成从SLB获取真实IP的字段即可,不然获取到的IP全都是阿里云SLB发过来处理过的IP,全都是一个网段的,根本没有办法起到封禁的效果)
cat /usr/local/openresty/nginx/conf/waf/accessip.lua
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
local ip_block_time=300 --封禁IP时间(秒) local ip_time_out=30 --指定ip访问频率时间段(秒) local ip_max_count=20 --指定ip访问频率计数最大值(秒) local BUSINESS = ngx.var.business --nginx的location中定义的业务标识符,也可以不加,不过加了后方便区分 --连接redis local redis = require "resty.redis" local conn = redis:new() ok, err = conn:connect("192.168.200.195", 6379) conn:set_timeout(2000) --超时时间2秒 ----如果连接失败,跳转到脚本结尾 if not ok then goto FLAG end --查询ip是否被禁止访问,如果存在则返回403错误代码 is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr) if is_block == '1' then ngx.exit(403) goto FLAG end --查询redis中保存的ip的计数器 ip_count, err = conn:get(BUSINESS.."-COUNT-"..ngx.var.remote_addr) if ip_count == ngx.null then --如果不存在,则将该IP存入redis,并将计数器设置为1、该KEY的超时时间为ip_time_out res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr, 1) res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out) else ip_count = ip_count + 1 --存在则将单位时间内的访问次数加1 -- if ip_count >= ip_max_count then --如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time res, err = conn:set(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, 1) res, err = conn:expire(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, ip_block_time) else res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr,ip_count) res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out) end end -- -- 结束标记 ::FLAG:: local ok, err = conn:close() |
2、在需要做访问限制的location里加两段代码即可,这里用刚才的/lua做演示
1 2 3 4 5 6 |
location /lua { set $business "lua"; access_by_lua_file "/usr/local/openresty/nginx/conf/waf/accessip.lua"; default_type text/plain; content_by_lua 'ngx.say("hello,lua!")'; } |
主要添加
access_by_lua_file /usr/local/openresty/nginx/conf/lua/access.lua;
让没一个请求都去调用这个lua脚本,注意路径和名字不要写错
set $business “lua” 是为了把IP放进redis的时候标明是哪个location的,可以不加
[root@test1 lua]# nginx -s reload #修改完后重启nginx
3、去访问192.168.1.222/lua 并一直按F5刷新
发现redis已经在统计访问lua这个网页ip的访问次数
这个key的过期时间是30秒,如果30秒没有重复访问20次这个key就会消失,所以说正常用户一般不会触发这个封禁的脚本
当30秒内访问超过了20次
发现触发脚本了,变成了403
这个脚本的目的很简单:一个IP如果在30秒内其访问次数达到20次则表明该IP访问频率太快了,因此将该IP封禁5分钟。同时由于计数的KEY在Redis中的超时时间设置成了30秒,所以如果两次访问间隔时间大于30秒将会重新开始计数
后续这个脚本可以继续优化,比如第一次封禁300秒,第二次再进去封3天,再进去永久封禁之类的,不过能力有限,还得继续研究,如果上面写的有什么错误,欢