通过HAProxy ACL,你可以定义用于阻止恶意请求,选择后端,重定向到HTTPS以及使用缓存对象的自定义规则。
当相关人员将负载均衡器添加到他们的基础架构中时,他们正在寻找能够扩展其网站和服务,获得更好的可用性以及获得更多宁静的夜晚,因为他们知道他们的关键服务不再是单点故障。然而,不久之后,他们意识到使用像HAProxy 这样的负载均衡器,他们可以智能的来检查传入的流量并在运行中做出决策。例如,你可以限制谁可以访问各种端点,将非https流量重定向到https,检测并阻止恶意机器和扫描程序,以及定义添加HTTP标头,更改URL或重定向用户的条件
HAProxy中的访问控制列表或ACL允许你测试各种条件并根据这些测试执行给定操作。这些条件几乎涵盖了请求或响应的任何方面,例如在其中搜索字符串或模式,检查它们来自的IP,最近的请求率(via stick tables),TLS状态等。你采取的操作可以包括制作路由决策,重定向请求,返回静态响应等等。在使用逻辑运算符(AND
,OR
,NOT
)在其他代理解决方案可能会很麻烦,HAProxy的ACL包含它们以形成更复杂的条件。
格式化ACL
有两种指定ACL的方法 - 命名ACL和匿名或内联ACL第一种形式是命名ACL:
acl is_static path -i -m beg /static
我们从acl关键字开始,后跟名称,后跟条件。这里我们有一个名为is_static的ACL。然后,此ACL名称可以与if和unless语句一起使用,例如use_backend be_static if is_static。当你要将给定条件用于多个操作时,建议使用此表单
acl is_static path -i -m beg /static
use_backend be_static if is_static
他的条件,路径-i -m beg / static,检查URL是否以/ static开头。你将在本文后面看到它如何与其他类型的条件一起使用
use_backend be_static if { path -i -m beg /static }
这与上面两行所做的相同,只是在一行中。对于内联ACL,条件包含在花括号内。
在这两种情况下,你可以将多个条件链接在一起。 ACL之间一个接一个地列出,其间没有任何内容将被认为是和一个和。如果两个ACL都为真,则整体条件才为真。
http-request deny if { path -i -m beg /api } { src 10.0.0.0/16 }
这将阻止10.0.0.0/16子网中的任何客户端访问以/ api开头的任何内容,同时仍能访问其他路径。
添加感叹号会反转条件:
http-request deny if { path -i -m beg /api } !{ src 10.0.0.0/16 }
现在只允许10.0.0.0/16子网中的客户端访问以/ api开头的路径,而禁止所有其他路径。
也可以从文件导入IP地址:
http-request deny if { path -i -m beg /api } { src -f /etc/haproxy/blacklist.acl }
在blacklist.acl中,你将使用CIDR表示法列出单个或一系列IP地址,如下所示:
192.168.122.3
192.168.122.0/24
你还可以使用||定义一个条件可以为真的ACL:
http-request deny if { path -i -m beg /evil } || { path -i -m end /evil }
这样,每个请求的路径以/evil开头(例如/evi/foo)或以/ evil结尾(例如/foo/evil)将被拒绝
你也可以执行相同的操作来组合命名ACL:
acl starts_evil path -i -m beg /evil
acl ends_evil path -i -m end /evil
http-request deny if starts_evil || ends_evil
对于命名ACL,多次指定相同的ACL名称将导致条件的逻辑OR,因此最后一个块也可以表示为:
acl evil path_beg /evil
acl evil path_end /evil
http-request deny if evil
这允许你组合AND和OR(以及命名和内联ACL)来构建更复杂的条件,例如:
http-request deny if evil !{ src 10.0.0.0/16 }
如果路径以/evil开头或结尾,这将阻止请求,但仅限于不在10.0.0.0/16子网中的客户端。
诸如弹性二叉树或EB树等保持ACL有着高性能特征。 例如,字符串和IP地址匹配依赖于EB树,这些EB树允许ACL处理数百万个条目,同时保持HAProxy最佳性能和效率。
从我们到目前为止看到的情况来看,每个ACL条件分为两部分 - 信息源(或获取),例如path和src以及它匹配的字符串。在这两个部分的中间,一个可以指定Flags(例如-i用于不区分大小写的匹配)和匹配方法(例如,请求在字符串的开头匹配)。 ACL的所有这些组件将在以下部分中进行扩展
Fetches
现在你已了解格式化ACL的基本方法,你可能希望了解可用于制定决策的信息源。 HAProxy中的信息源称为fetch,这些允许ACL获取要处理的信息
以下是一些更常用的Fetches:
src : 返回发出请求的客户端IP地址
path : 返回客户端请求的路径
url_param(foo) : 返回给定URL参数的值
req.hdr(foo) : 返回给定HTTP请求标头的值(例如,User-Agent或Host)
ssl_fc : 如果连接是通过SSL进行的,则返回true的布尔值,HAProxy在本地解密它
Converters
通过Fetches获得一条信息后,你可能希望对其进行转换。Converters由逗号分隔,如果你有多个Converters,则可以与其他Converters分隔,并且可以多次链接在一起。
某些Converters(例如lower和upper)由它们自己指定,而其他Converters则传递给它们。如果需要参数,则在括号中指定。例如,要获取从/static的路径的值,删除,你可以使用带有正则表达式和替换的regsub Converters作为参数:
path,regsub(^/static,/)
与Converters一样,有各种各样的Converters,但下面是一些比较流行的Converters:
lower :将示例的大小写更改为小写
upper :将示例的大小写更改为大写
base64 :Base64对指定的字符串进行编码(适用于匹配二进制样本)
field :允许你Fetches类似于awk的字段。例如,如果你将“a | b | c”作为样本并在其上运行字段(|,3),则将留下“c”
bytes :给定偏移量和长度作为参数,从输入二进制样本中Fetches一些字节
map :在指定的映射文件中查找示例并输出结果值
Flags
你可以在单个ACL中放置多个Flags,例如:
path -i -m beg -f /etc/haproxy/paths_secret.acl
这将基于路径的开头执行不区分大小写的匹配,并与存储在指定文件中的模式进行匹配。没有像获取/Converters类型那样多的Flags,但是有很多种类。
以下是一些常用的:
-i :执行不区分大小写的匹配(因此FoO的样本将匹配Foo的模式)
-f :而不是匹配字符串,匹配ACL文件。此ACL文件可以包含IP,字符串,正则表达式等列表。只要列表不包含正则表达式,该文件就会被加载到b-tree格式中,几乎可以立即处理数百万个项目的查找
-m :指定匹配类型。这将在下一节中详细介绍。
如果从文档的ACL Basics部分向下滚动,你会发现其他几个
Matching methods
现在你有一个来自Converters和fetches的示例,例如通过路径请求的URL路径,以及通过硬编码路径/evil匹配的内容。要将前者与后者进行比较,你可以使用几种匹配方法中的一种。和以前一样,有很多匹配方法,你可以通过向下滚动(除了Flags之外)在文档的ACL Basics部分中查看完整列表。以下是一些常用的匹配方法:
str :执行精确的字符串匹配
beg :使用模式检查字符串的开头,因此“foobar”的样本将匹配“foo”但不是“bar”的模式。
end :检查带有模式的字符串的结尾,因此foobar的样本将匹配bar的模式但不匹配foo。
sub :子串匹配,因此foobar的样本将匹配模式foo,bar,oba。
reg :将模式作为样本的正则表达式进行比较。警告:与其他匹配方法相比,这是CPU饥饿,除非没有其他选择,否则应该避免。
found :这是一个根本不采用模式的匹配。与之匹配的是true如果发现样品,false否则。这可用于(作为一些常见示例)查看是否存在header(),是否设置了cookie(),或者是否找到了map()中的样本。req.hdr(x-foo) -m foundcook(foo) -m foundsrc,map(/etc/hapee-1.8/ip_to_country.map-m
len :返回样本的长度(所以带有-m len 3的foo样本将匹配)
到目前为止,你可能已经注意到使用path -m beg /evil
比较我们的预期路径/evil与我们使用匹配方法检查的样本的开头beg
。在许多地方,你可以使用在一个参数中组合样本获取和匹配方法的速记。在这个例子中path_beg /foo
并且path -m beg /foo
完全相同,但前者更容易键入和读取。并非所有Fetches都具有内置匹配方法的变体(事实上,大多数都没有),并且存在一个限制,即如果使用Converters链接Fetches,则必须使用Flags指定它(除非链上的最后一个Converters)有匹配变体,大多数没有)。
如果没有所需匹配方法的获取变体,或者如果使用Converters,则可以使用-m
上一节中提到的Flags来指定匹配方法。
关于ACL
让我们快速了解可以由ACL控制的HAProxy中的常见操作。这并不是为了给你提供这些规则可以使用的所有条件或方法的完整列表,而是为你在遇到ACL可以提供帮助的内容时提供想象力。
使用http请求重定向重定向
此规则有许多变体,所有这些变量都会向客户端返回301/302响应,告诉他们在另一条路径下请求。所有这些都允许使用%[]
语法指定的日志格式规则在字符串中使用以允许动态重定向。
该命令http-request redirect location
设置整个URI。例如,要将非www域重定向到其www,你可以使用:
http-request redirect location http://www.%[hdr(host)]%[capture.req.uri] unless { hdr_beg(host) -i www }
在这种情况下,我们的ACL hdr_beg(host) -i www
确保客户端被重定向,除非它们的Host HTTP头已经以www开头。
该命令http-request redirect scheme
改变了请求的方案,而其余部分则保留了其余部分。这允许简单的http-to-https重定向行:
redirect scheme https if !{ ssl_fc }
在这里,我们的ACL !{ ssl_fc }
检查请求是否未通过https进入。
该命令http-request redirect prefix
允许你指定将请求重定向到的前缀。例如,下面的行会导致没有一个URL路径的所有请求与开始/foo被重定向到/foo/ {original URI here}:
http-request redirect prefix /foo if !{ path_beg /foo }
对于这些中的每一个,可以添加代码参数以指定响应代码。如果未指定,则默认为302。支持响应代码是301,302,303,307,和308。例如:
redirect scheme code 301 https if !{ ssl_fc }
这会将http请求重定向到https并告诉客户端他们不应该继续尝试http。或者对于更安全的版本,你可以通过注入Strict-Transport-Security标头http-response set-header
。
使用use_backend选择后端
在HTTP模式下
该use_backend
行允许你指定使用另一个后端的条件。例如,要将HAProxy统计信息网页的流量发送到统计信息后端,你可以use_backend
与ACL一起检查URL路径是否以/ stats开头:
use_backend be_stats if { path_beg /stats }
为了更加有趣,后端名称可以是动态的日志格式规则。在下面的示例中,我们将路径放在地图中并使用它来生成后端名称:
use_backend be_%[path,map_beg(/etc/haproxy/paths.map)]
如果文件paths.map包含/api api作为键值对,则流量将被发送到be_api,将静态be_与字符串api组合。如果没有任何映射条目匹配,并且你已将可选的第二个参数(默认参数)指定给map函数,则将使用该默认值
use_backend be_%[path,map_beg(/etc/haproxy/paths.map, mydefault)]
在这种情况下,如果映射文件中没有匹配项,则将使用后端be_mydefault。否则,流量将自动通过此规则搜索use_backend
匹配的另一个规则或为default_backend行的配置。
在TCP模式下
我们还可以为TCP模式流量做出路由决策,例如,如果流量是SSL,则将流量定向到特殊后端:
tcp-request inspect-delay 10s
use_backend be_ssl if { req.ssl_hello_type gt 0 }
请注意,对于tcp级路由决策,当需要来自客户端的数据(例如需要检查请求)时,该inspect-delay
语句需要避免HAProxy在没有来自客户端的任何数据的情况下传递阶段。除非客户端保持静止10秒钟,否则它不会等待整整10秒钟。一旦它可以决定缓冲区是否具有某种类型的SSL消息,它就会继续前进。
使用http-request set-header设置HTTP标头
向请求添加HTTP标头有多种选择(对客户端透明)。将此与ACL组合使我们只能在给定条件为真时设置标头
add-header :添加新标头。如果客户端发送了相同名称的标头,则会忽略它,添加同名的第二个标头。
set-header :将以相同的方式添加新标头add-header,但如果请求已经具有相同名称的标头,则将覆盖该标头。适用于客户端可能想要篡改的安全敏感标记。
replace-header :应用指定标头的正则表达式替换(例如,将假cookie注入cookie标头)
del-header :从请求中删除指定名称的任何标头。在添加新标头(或其中使用的任何自定义标头名称)之前删除x-forwarded-for标头很有option forwardfor用。
使用http-request set-path更改URL
这允许HAProxy修改客户端请求的路径,但对客户端透明。它的值接受日志格式规则,因此你可以使请求的路径动态化。例如,如果你想在不通知客户端的情况下向所有请求添加/foo/(如上面的重定向示例),请使用:
http-request set-path /foo%[path] if !{ path_beg /foo }
还有set-query
,它改变了查询字符串而不是路径,并且set-uri
将路径和查询字符串设置在一起,这是变体。
使用http-response set-map更新map file
这些操作并不经常使用,但在动态调整HAProxy映射方面开辟了有趣的可能性。这可以用于诸如让登录服务器告诉HAProxy从那时起将客户端(在这种情况下是会话cookie)请求发送到另一个后端的任务:
http-request set-var(txn.session_id) cook(sessionid)
use_backend be_%[var(txn.session_id),map(/etc/haproxy/sessionid.map)] if { var(txn.session_id),map(/etc/haproxy/sessionid.map) -m found }
http-response set-map(/etc/haproxy/sessionid.map) %[var(txn.session_id)] %[res.hdr(x-new-backend)] if { res.hdr(x-new-backend) -m found }
default_backend be_login
现在,如果后端在响应中设置x-new-backend头,HAProxy将使用客户端的sessionid cookie将后续请求发送到指定的后端。变量被用作,否则,响应阶段HAProxy无法访问请求cookie - 你可能需要记住HAProxy在启动期间会警告的其他类似问题的解决方案
还有与del-map
基于ACL条件删除映射条目有关。
与大多数操作一样,
http-response set-map
有一个相关的操作http-request set-map
。这作为伪API可用于允许后端添加和删除映射条目。
使用http请求缓存使用缓存
HAProxy 1.8的新功能是小对象缓存,允许基于ACL缓存资源。这与http-response cache-store
一起允许你在HAProxy的缓存系统中存储选择请求。例如,鉴于我们已经定义了一个名为icons的缓存,以下内容将存储以/icons开头的路径的响应,并在将来的请求中重用它们
http-request set-var(txn.path) path
acl is_icons_path var(txn.path) -m beg /icons/
http-request cache-use icons if is_icons_path
http-response cache-store icons if is_icons_path
使用ACL来阻止请求
现在你已经简单的了解了ACL,现在是时候做一些请求阻止了!
该命令http-request deny
向客户端返回403并立即停止处理请求。这经常用于DDoS/Bot缓解,因为HAProxy可以在不打扰Web服务器的情况下拒绝大量请求。
类似于此的其他响应包括http-request tarpit
(保持请求挂起,直到超时tarpit到期,然后返回500 - 通过重载连接表来减慢访问的速度,http-request silent-drop
(让HAProxy停止处理请求)但是告诉内核不要通知客户端 - 从客户端角度打开连接,但是从HAProxy角度关闭;要注意状态防火墙)。
使用deny和tarpit,你可以添加deny_status
Flags来设置自定义响应代码,而不是使用开箱即用的默认403/500。例如,使用http-request deny deny_status 429
将导致HAProxy 使用错误429:Too Many Requests响应客户端。
在以下小节中,我们将提供许多静态条件,阻塞流量可能对其有用。
HTTP协议版本
许多攻击使用HTTP 1.0作为协议版本,因此如果是这种情况,使用内置ACL很容易阻止这些攻击HTTP_1.0
http-request deny if HTTP_1.0
用户代理字符串的内容
我们还可以检查User-Agent标头并拒绝它是否与指定的字符串匹配
http-request deny if { req.hdr(user-agent) -m sub evil }
如果-m sub
用户代理请求标头的一部分包含字符串evil,则该行将拒绝该请求。删除-m sub
,留下你req.hdr(user-agent) evil
作为条件,它将是一个完全匹配而不是substring。
用户代理字符串的长度
一些攻击者将尝试使用随机md5sum绕过正常的用户代理字符串,这可以通过长度识别并立即阻止:
http-request deny if { req.hdr(user-agent) -m len 32 }
攻击者的攻击可能会有很多不同,因此你可以依赖合法用户代理更长的事实,同时将其设置为最小长度:
http-request deny if { req.hdr(user-agent) -m len le 32 }
然后,这将阻止任何具有短于32个字符的用户代理标头的请求。
路径
如果攻击者滥用合法客户端没有的特定URL,则可以根据路径阻止:
http-request deny if { path /api/wastetime }
或者,你可以阻止攻击者访问隐藏文件或文件夹:
http-request deny if { path -m sub /. }
延伸阅读
linuxea:haproxy 1.9中的多线程linuxea:haproxy1.9 了解四个基础部分linuxea:haproxy1.9日志简介linuxea: 使用HAproxy 1.9 Runtime API进行动态配置linuxea: 探索 HAproxy 1.9 统计页面