助你更了解CDN
当前位置:网站首页 > CDN软件 > 正文

自建CDN实战经验合集之——基础waf防护系统的实现

作者:CDN发布时间:2017-05-05分类:CDN软件浏览:60评论:0


导读:...





1 概述



WEB安全中除典型攻击外(如SQL注入、文件上传、敏感文件泄漏等),近些年来CC攻击也逐渐走向”正轨”,占有相当比例。结合业务场景,我们也使用ngx_lua实现了CDN waf防护系统,本文主要分享传统Nginx防CC的一些经验,以及自建CDN waf防护系统中基础模块的实现。




2 Nginx CC防护经验



早期线上做CC防护,通常是限制IP访问、同一IP请求数、并发数来实现,主要模块有:



通常结合Core模块limite_rate做限速、map模块做变量映射、Geo模块做白名单策略等等。


具体Nginx模块的详细简介以及安装过程,则不再赘述,有兴趣的看客可以直接查看官方文档。
http://nginx.org/en/docs/ 


2.1 IP访问限制


针对IP做访问限制,一般可使用allow/deny指令,即http_access_module模块,默认自带(除非编译显式指明--without-http_access_module)。被deny拦截的请求返回403。


参考实例:
location /health_status {  allow 127.0.0.1;        #放行IP  deny 192.168.10.25/32;    #拒绝指定IP  allow 192.168.10.0/24;    #放行网段  #deny 2001:0db8::/32;   #支持ipv6  #deny unix:/tmp/test.sock; #1.5.3+版本支持UNIX域          deny  all;                #默认规则
经验:
(1) 规则匹配自上而下、精细规则要靠前写、不要漏写默认规则;


(2) 通常将allow、deny条目写成规则文件如blockip.conf ,通过 include blockip.conf引入, 可在http、server、location及limit_except配置段中引入;


(3) 更改规则需重载nginx生效,由于无法预知攻击者IP,并不适合动态防CC。适合针对特定站点/路径 做访问保护,如CDN健康状态页只允许本机、中央机访问。


2.2 请求数限制


ngx_http_limit_req_module模块可通过预先定义的键值来限制请求处理的频率,默认自带(除非编译显式指明--without-http_limit_req_module)。如果定义客户端IP为键值,则可以针对同一IP单位时间内的请求数做限制(无论这些请求是否处理),是”请求数量层面限制”。


限流原理是” leaky bucket”(漏桶算法):服务器以恒定的速率处理请求,超出处理速率的请求放在burst队列中延迟处理,严重超出的请求(大于恒定处理速率+burst队列上限)则直接丢弃。



参考示例:针对动态页面做请求限制
http {  ...  limit_req_zone $binary_remote_addr zone=dynamic_req_limit:20m rate=20r/s;  server {    ...    location ~* .*\.(aspx|jsp|php)($|\?) {      ...      limit_req zone=dynamic_req_limit burst=100;      limit_req_log_level error;      limit_req_status 403;    }  } }

配置说明:
(1) 定义$binary_remote_addr为KEY,即客户端IP作为"请求标识"(用于计数);


(2) zone=req_limit:20m 生成大小20m、名为dynamic_req_limit的内存区域, 用于存储访问频次信息;


(3) rate=20r/s 表示相同"请求标识"(即IP相同)的请求、每秒处理20个;


(4) zone=dynamic_req_limit burst=100在location配置段下,是指针对动态请求(aspx|jsp|php)限制同一IP每秒最多处理20次请求,burst为缓冲队列,超出20小于120的请求可进入队列延迟处理,超出120以上的请求会被丢弃并默认返回503;


(5) limit_req_log_level error 用于记录日志0.8.18+版本支持;


(6) limit_req_status 403 被拒绝的请求(即超出rate+burst总上限)默认返回503,在1.3.15+版本后支持自定义状态码,这里修改为403。


经验:
(1) 1.7.6+版本前,KEY只能是单个变量;更高版本KEY支持多个变量、文本等组合,CDN场景下通常是将”IP+访问URI”作为唯一标识,来做策略限制,如:
limit_req_zone $binary_remote_addr$request_uri zone=req_limit:20m rate=1r/s;

(2) limit_req 指令可配置nodelay参数,若未设置nodelay,则当前时刻处于burst队列的请求需到下一秒执行;设置nodelay会强制将当前burst队列请求也同一时刻处理(增大瞬间吞吐),即此刻请求要么被处理、要么被丢弃,burst队列为空;
limit_req zone=dynamic_req_limit burst=100 nodelay;

(3) $binary_remote_addr为固定4字节,相比$remote_addr(7~15字节)节约内存;状态位(即频次信息)32位平台占用64字节,64位平台占用128字节,因此64位平台上,1M zone可保存约8000个IP状态。具体zone大小可根据key值大小、rate限制频率及Web服务QPS来推算;


(4) $binary_remote_addr仅代表TCP连接的对端IP地址,在前端有代理转发、负载均衡器、CDN场景等情况下,该值不能作为req_limit key(并非用户真实IP),比如通过HTTP header提取用户真实IP,参考如下
map $http_x_forwarded_for  $ClientRealIp {  ""    $remote_addr;  ~^(?P<firstAddr>[0-9\.]+),?.*$    $firstAddr; } limit_req_zone $ClientRealIp zone=req_limit:20m rate=1r/s;2.3 并发数限制

ngx_http_limit_conn_module模块其配置、用法几乎和limit_req_module一样,不同在于该模块只针对正在被处理的请求(即请求头已被nginx完全读入)来计数。如果定义客户端IP为键值,则可针对同一IP同一时刻正在处理的并发连接数做限制,即”并发处理速度层面限制”。


参考示例:
http {  ...  limit_conn_zone $binary_remote_addr zone=perip:10m;  limit_conn_zone $server_name zone=perserver:10m;  server {    ...    location /download/ {      ...      limit_conn perip 5;      limit_conn perserver 1000;      limit_rate 1024k;      limit_conn_status 503                }  } }配置说明:针对/download/下载路径限制该虚拟主机(vhost)每秒最高并发1000个,同一IP并发连接处理数不超过5个,单个连接限速1024k(针对连接限速,非IP限速)。这里limit_rate是ngx Core模块中提供的指令。


使用经验可参考ngx_http_limit_req模块,1.1.8+版本前使用limit_zone name $variable size指令定义,在1.7.6版本后该指令正式废弃,统一使用limit_conn_zone $variable zone=name:size定义。


2.3 CC防护小结


在线上经历了多次CC攻击后,发现典型的CC攻击有如下特征:
(1) 一段时间内大量来自同一IP/网段的请求;
(2) 一段时间内同一URL请求数无理由、突发式骤增;
(3) 具有其他特征的不正常请求、突增,如User-Agent、Referer等等。


早期线上组合使用limit_req和limit_conn的确可以起到一定的CC防护效果,但比较乏力:
(1) 本质是全局限流,功能上单一死板,可能影响到正常用户(阈值设置难);
(2) CC攻击源IP海量时,难以触发限流,防护效果差;且无法针对CC攻击特征来实现防护(如针对指定IP拦截、针对User-Agent、URL等特征拦截)。




3 基于Openresty实现waf



随着业务场景越来越复杂,基于 modsecurity(一款Nginx 模块)做waf防护、limit_req+limit_conn做CC防护这种”大杂烩套餐”也越来越吃力,业务场景契合度低、维护成本大。在参考CloudFlare WAF思路以及Openresty在UPYUN的应用等大厂做法后,最终选择Openresty、并尝试实现waf防护。


3.1 Openresty与ngx_lua模块


提到Openresty,不得不说ngx_lua模块(全名ngx_http_lua_module)。Ngx_lua模块将lua解释器集成进Nginx,在保证Nginx高性能、高并发前提下可通过lua脚本快速实现业务逻辑,该模块可直接编译进Nginx中。
而OpenResty是将Nginx核心、LuaJIT等许多优良Lua库和Nginx第三方模块(包含ngx_lua)打包在一起,亮点是默认集成了Lua开发环境。其详细简介和安装过程不再赘述,有兴趣的可以参考官方文档: 
Openresty https://openresty.org/
Ngx_http_lua_module https://github.com/openresty/lua-nginx-module 


Ngx_lua提供的”钩子函数”贯穿着整个Nginx HTTP请求处理过程,通过ngx_lua钩子、调用lua脚本执行,能够在Nginx HTTP请求处理的各个阶段实现lua控制,可玩性很高。详细说明可参考https://openresty.org/download/agentzh-nginx-tutorials-zhcn.html



3.2 实现user-agent过滤


谈了这么多,不妨用ngx_lua小试牛刀,实现一个简单的过滤功能。
Nginx针对指定user-agent过滤,可通过if条件判断实现,画风如下:
if ($http_user_agent ~* 'Baiduspider|Googlebot|iaskspider') {return 403;}而ngx_lua实现画风如下:ywjt.org.conf(nginx配置文件)
server {  listen 80;  server_name ywjt.org;  location / {    default_type text/html;    access_by_lua '      local rules = "(Baiduspider|HTTPtrack)"      local ua = ngx.var.http_user_agent      if ngx.re.match(ua, rules, "isjo") then        ngx.exit(403)      end';    content_by_lua 'ngx.say("hello,world!")';  } }简要说明:
通过content_by_lua,生成http响应内容。其中ngx.say(“hello,world”)为nginx函数打印hello,world!
通过access_by_lua,实现user-agent过滤。

定义规则(正则语法) rules = “(Baiduspider|HTTPtrack)”
获取请求头 ua = ngx.var.http_user_agent (ngx_lua模块支持lua脚本直接引用nginx变量)
正则匹配过滤 ngx.re.match(ua,rules,”isjo”) 如匹配成功,则直接nginx返回403




过滤效果:



一个简单的针对user-agent过滤功能就实现了。看上去比在nginx中写if判断复杂,但借助ngx_lua内置函数、通过lua脚本(灵活操纵nginx变量、做逻辑判断、甚至操作redis/mysql等等),能实现更为高级复杂的控制。




3.3 自建waf模块


基于Openresty,我们在第三方开源防护模块的基础上二次开发,实现了CDN waf防护模块,功能如下:
支持IP白名单和黑名单功能, 支持动态添加、删除
支持CC防护(7层拦截+4层iptables限流)
支持URL黑名单拦截
支持请求参数防护
支持User-Agent、Referer、Cookie特征拦截
支持日志记录、切割
支持友好拦截页呈现
支持API接口,默认返回JSON格式


3.3.1 整体架构



日志模块是全局的,图中未作展示。


文件结构:



3.3.2 自建waf模块


Waf防护逻辑并不复杂,这里以User-Agent防护为例


Step1: init.lua初始化时解析全局配置文件
require 'config' local optionIsOn = function(options) return options == "on" and true or false end Mod_UA       = optionIsOn(Mod_UA)config.lua配置文件中 Mod_UA=”on” 即默认开启UA防护模块, 因此Mod_UA=true


Step2:init.lua初始化时加载并解析规则文件
require 'config' rulepath = RulePath -- 规则解析函数(解析 rulepath/* 中的规则到对应table中) function parse_rule(var)    file = io.open(rulepath .. '/' .. var, "r")    if file == nil then        return    end    t = {}    for line in file:lines() do        table.insert(t, line)    end    file:close()    return (t) end ua_rules = parse_rule('user-agent')parse_rule函数解析规则文件,规则文件存放目录在config.lua中rulepath指明
这里解析 rules/user-agent规则文件到 ua_rules中(本质是一个Lua Table数据结构)


Step3:init.lua初始化时加载UA防护代码
local match = string.match local ngxmatch = ngx.re.match local unescape = ngx.unescape_uri local get_headers = ngx.req.get_headers -- UA防护模块 function ua()    if Mod_UA then        local ua = ngx.var.http_user_agent        if ua ~= nil then            for _, rule in pairs(ua_rules) do                if rule ~= "" and ngxmatch(ua, rule, "isjo") then                    log('UA_Deny', ngx.var.request_uri, "-", rule)                    say_html()                end            end        end        return false    end end --友好显示页模块 function say_html()    if Mod_Redirect then        ngx.header.content_type = "text/html"        ngx.status = ngx.HTTP_FORBIDDEN        ngx.say(html)        ngx.exit(ngx.status)    else        ngx.exit(403)    end end获取http请求头中user-agent,遍历ua_rules中所有规则、逐一做nginx正则匹配。和前文简单实现的user-agent过滤是不是很像呢?


UA、URL、Referer等防护模块本质都是一个套路。那么多个模块如何协同工作呢?是通过waf.lua实现控制流程,这里也贴出部分代码供参考


waf.lua实现流程控制
-- 处理逻辑 if waf_check() then    --首先检查waf是否开启,可通过api远程启用/禁用 elseif ipdeny() then    --其次IP认证模块检查 elseif ccdeny() then elseif ua() then elseif url() then elseif args() then elseif referer() then elseif cookie() then else    return end最后只需要在Nginx配置文件,通过access_by_lua_file waf.lua引入就行了!


3.3.3 CC防护模块实现


CC防护模块依赖于IP认证模块,实现了4层和7层防护。


IP认证模块:
(1) 使用waf_limit字典(即key-value,这里ip为key),基于value值区分IP黑白名单
(2) value=-1 IP白名单,直接放行
(3) value=-2 IP黑名单,7层拒绝


防护代码:
local ok, err = waf_limit:get(ip) if ok == -1 then  return true            --是IP白名单 跳出waf防护、正常处理 elseif ok == -2 then  log('Ip_Deny', ngx.var.request_uri, "-", "BlackIP")  say_html()            --是IP黑名单 log记录日志、并7层拒绝访问     end

CC防护模块: 复用waf_limit字典,用于针对IP访问计次
4层防护模块: 使用waf_limit4字典,用于记录IP因触发CC被封禁的次数
太过抽象,不妨举个例子。假设config.lua配置如下:
-- IP验证模块 Mod_IPDeny  = "on" ipWhite = "ipwhite.txt" ipBlack = "ipblack.txt" -- CC防护模块 (依赖Mod_IPDeny模块) Mod_CCDeny  = "on" CCrate      = "60/500/300" -- L4防护模块 (依赖Mod_CCDeny模块) Mod_L4P     = "off" L4Prate     = "3600/5/30/150/300" shellpath   = "/usr/local/openresty/nginx/luascript/waf/l4protect.sh"具体逻辑:
(1) 用户IP属于黑名单或白名单 -> IP认证模块处理拦截或放行;
(2) 用户IP不属于黑白名单;
a) 单IP、60秒内、请求数>500、则7层拦截300秒 [该IP 触发CC拦截次数+1];
b) 单IP、3600秒内、CC拦截次数≥5、通过iptables限流(30秒、150令牌环、超出丢弃、规则生效300秒) [该IP触发CC拦截次数清0]。


核心代码
if Mod_CCDeny then  local ip = getClientIp()        local waf_limit = ngx.shared.waf_limit  local req, _ = waf_limit:get(ip)  if req then    if req > cc_times then      local waf_limit4 = ngx.shared.waf_limit4      local cc, err = waf_limit4:get(ip)      if cc then        if Mod_L4P then      if cc+1 >= l4_times then        local command = '/bin/bash ' .. shellpath .. ' ' .. ip .. ' ' .. l4_opt1 .. ' ' .. l4_opt2 .. ' ' .. l4_expire ..' &'        os.execute(command)        log('L4P_Deny', ngx.var.request_uri, "-", "L4P_Success")        waf_limit4:set(ip, nil)            -- clean up        waf_limit:set(ip, nil)             -- clean up          say_html()      else        waf_limit4:incr(ip, 1)      end        end          else        waf_limit4:set(ip, 1, l4_interval)      end      waf_limit:set(ip, -2, cc_expire)      log('CC_Deny', ngx.var.request_uri, "-", "CC_Deny")      say_html()    else      waf_limit:incr(ip, 1)    end  else    waf_limit:set(ip, 1, cc_interval)                    end end

4层防护是调用l4protect.sh脚本通过iptables recent模块实现拦截攻击的,iptables限流方法众多,实现并不复杂,这里不再深入展开。


3.3.4 让waf融入架构


waf模块源于生产需求,自然也得融入业务架构、形成闭环。下面是我们CDN业务场景中waf防护系统的典型架构: 

(1) 节点部署waf用于保护自身,产生的日志打上”waf”标签、接入统一日志平台;
(2) 日志平台针对waf日志集中分析、产生告警同步到CDN运维后台,CDN运维后台以waf_api调用下发规则(如封禁指定IP)到节点,形成闭环。


节点waf日志:



统一日志平台:



Waf_API接口:

waf日志的价值不仅在于回溯,更在于分析、提炼出防护规则,防护规则下发至节点,又在一定程度提升了节点的识别准确性和防护能力。


3.3.5 线上压测


af模块上线前,我们也在公网环境进行了压测,压测服务器为8核、16GB,模拟1000并发、100W请求,压测数据如下:



总体来说,启用waf后性能损失在可接受范围内,线上业务还可根据实际需求开关模块、优化规则等。


waf原理大体是通过正则匹配(黑名单)进行拦截,规则通常是预先定义的。当然也支持用lua共享字典、redis等实现规则动态管理,但由于正则缓存的问题,动态规则对性能影响较大(尤其是cdn海量小文件请求场景)、另外引入redis所带来的开销也较大,为保证高性能,通常不采用。






4 总结



本文主要简述了Nginx 常见CC防护模块以及自建CDN中基础waf防护的实现,仅是引玉之砖。事实上,市面上大部分waf防护产品也是类似思路实现的。通过ngx_lua模块能实现更为高级全面、贴近实际业务场景的waf防护系统。






近期文章
















END






全中国只有不到1% 的人关注了运维军团

你是个有眼光的人!




(由于交流群人数已超100人,需要进群的小伙伴可以添加运维小编的微信:qq834775039)















如果你喜欢我们的文章,请转发到朋友圈
 公众号
ywjtshare
运维军团
 专注运维技术与传承,分享丰富原创干货





欢迎 发表评论:

  • 请填写验证码