Caddy集成OWASP Coraza WAF:开源Web应用防火墙实战配置指南
1. 项目概述与核心价值
最近在折腾个人项目,后端用的是Go写的,顺手就选了Caddy作为反向代理服务器,图的就是它配置简单、自动HTTPS。但项目上线前,安全这块心里总不踏实,尤其是防SQL注入、XSS这些常见的Web攻击。市面上商业WAF(Web应用防火墙)功能强大,但价格不菲,对于个人项目或者小团队来说,成本压力不小。于是我开始寻找开源的、能和Caddy无缝集成的WAF方案,最终锁定了OWASP Coraza。
OWASP Coraza是什么?简单说,它是一个用Go语言编写的、开源的Web应用防火墙引擎。它的最大优势是100%兼容ModSecurity的SecRules语法和OWASP核心规则集(CRS)。这意味着,所有为ModSecurity编写的、经过实战检验的庞大规则库,你都可以直接拿来用在Coraza上。而coraza-caddy这个模块,就是专门为Caddy服务器打造的Coraza集成插件。它让你能在Caddy的配置文件中,像启用一个普通模块一样,轻松地为你的网站或API加上一层强大的WAF防护。
我花了几天时间,从零开始研究、配置、测试,最终成功将Coraza WAF集成到了我的Caddy服务中,并且全程免费。这篇文章,就是我这趟“踩坑”之旅的完整记录。我会详细拆解从环境准备、编译Caddy、配置规则、到实战测试和问题排查的全过程。无论你是个人开发者想保护自己的博客,还是运维同学在为公司的轻量级服务寻找安全方案,这篇指南都能给你提供一条清晰的、可复现的路径。
2. 环境准备与Caddy编译
在开始配置WAF之前,我们得先有一个“载体”——也就是集成了Coraza插件的Caddy服务器。官方提供了几种方式,但最可靠、最灵活的还是自己用xcaddy工具编译。
2.1 安装Go与xcaddy
Coraza和这个插件都是用Go写的,所以第一步是安装Go语言环境。去Go官网下载对应你操作系统的最新稳定版(目前是1.21+),安装过程很简单,一路下一步就行。安装完成后,打开终端,运行go version确认安装成功。
接下来安装xcaddy,这是Caddy官方推荐的定制化构建工具。
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest这条命令会把xcaddy安装到你的$GOPATH/bin目录下。确保这个目录在你的系统PATH环境变量里,这样你就可以在任意位置使用xcaddy命令了。如果遇到“command not found”,可能需要手动将$GOPATH/bin添加到你的shell配置文件(如~/.bashrc或~/.zshrc)中,然后执行source命令生效。
2.2 编译带Coraza插件的Caddy
有了xcaddy,编译就一行命令的事。但这里有几个细节需要注意,直接关系到编译的成功与否。
基础编译命令:
xcaddy build --with github.com/corazawaf/coraza-caddy/v2这条命令会从Caddy官方仓库拉取最新源码,并集成coraza-caddy/v2插件,最终在当前目录生成一个名为caddy的可执行文件。
编译过程中的常见问题与解决:
网络问题导致依赖下载失败:Go模块依赖需要从
proxy.golang.org等镜像站下载。如果你在国内,可能会遇到超时。解决方案是设置Go模块代理:go env -w GOPROXY=https://goproxy.cn,direct设置后再执行编译命令。
版本兼容性问题:
coraza-caddy插件有其兼容的Caddy版本范围。如果xcaddy默认拉取的最新版Caddy与插件不兼容,编译会报错。这时可以指定Caddy版本:xcaddy build v2.7.6 --with github.com/corazawaf/coraza-caddy/v2具体的兼容版本需要去
coraza-caddy的GitHub仓库的go.mod文件里查看。在我撰写本文时,v2.5.0版本的插件与Caddyv2.7.x兼容良好。编译平台指定:如果你是在macOS或Windows上编译,但最终要在Linux服务器上运行,需要指定目标操作系统和架构:
xcaddy build --with github.com/corazawaf/coraza-caddy/v2 --output ./caddy-linux --goos=linux --goarch=amd64这样生成的
caddy-linux文件就可以直接上传到Linux服务器运行了。
编译成功后,你可以通过./caddy version来验证插件是否成功集成。输出信息中应该能看到包含coraza_waf的模块列表。
实操心得:建议在本地开发环境(如Linux虚拟机或WSL2)中完成编译,这样环境更接近生产服务器,能提前发现一些依赖库的问题。编译好的二进制文件可以直接扔到服务器上运行,无需在服务器上安装完整的Go编译环境,非常干净。
3. Coraza WAF核心配置详解
编译好Caddy后,核心工作就转向了Caddyfile的配置。Coraza WAF的所有能力,都通过coraza_waf这个配置块来开启和定义。
3.1 配置块结构与强制指令
首先,有一个至关重要的前提:为了让Coraza插件能拦截和处理所有请求,必须在全局配置或站点配置的最开始,使用order指令将其优先级设为最高。
{ # 这个指令必须放在最前面! order coraza_waf first } yourdomain.com { # 你的其他配置... }如果忘记这一步,Coraza可能会在其他模块(如file_server、reverse_proxy)处理完请求后才生效,导致拦截失败。
3.2directives字段:规则加载的基石
coraza_waf配置块的核心是directives字段,它接受一个多行字符串,里面填写的就是ModSecurity格式的规则。
基本语法示例:
http://localhost:8080 { order coraza_waf first coraza_waf { directives ` # 启用规则引擎 SecRuleEngine On # 定义请求体处理大小 SecRequestBodyLimit 13107200 # 一条简单的测试规则:访问 /test 路径则拒绝 SecRule REQUEST_URI “@streq /test” “id:100,phase:2,deny,status:403,log” ` } respond “Hello, World!” }directives里的内容会被逐条解析。SecRuleEngine On是引擎开关,必须开启。SecRequestBodyLimit设置请求体大小限制,超过的请求体会被拒绝或部分处理,这个值需要根据你的应用实际情况调整。
规则语法速览:一条典型的SecRule包含三个部分:
- 变量(Variable):如
REQUEST_URI(请求URI)、ARGS(GET/POST参数)、REQUEST_HEADERS。 - 操作符(Operator):如
@streq(字符串相等)、@contains(包含)、@rx(正则表达式匹配)。 - 动作(Action):如
deny(拒绝)、pass(通过)、log(记录日志),以及id(规则ID)、phase(处理阶段)、status(返回状态码)等参数。
3.3load_owasp_crs字段:加载核武器
手动写规则既累又容易遗漏。Coraza最大的优势就是能直接使用OWASP CRS。通过设置load_owasp_crs字段为true,插件会自动加载内置的CRS规则变量和配置文件路径映射,为后续Include指令铺平道路。
加载CRS的标准配置模式:
coraza_waf { load_owasp_crs directives ` SecRuleEngine On # 1. 包含CRS的推荐配置文件,设置一些基础变量和动作 Include @coraza.conf-recommended # 2. 包含CRS设置文件,这里是示例,生产环境应使用自定义的crs-setup.conf Include @crs-setup.conf.example # 3. 包含所有CRS规则文件 Include @owasp_crs/*.conf ` }这里的@coraza.conf-recommended和@crs-setup.conf.example是插件内置的配置别名,指向特定的文件。@owasp_crs/*.conf则会加载所有CRS规则。
注意事项:直接使用
@crs-setup.conf.example是不安全的,因为它包含的是示例配置。生产环境中,你必须将其复制出来,根据官方CRS文档进行调优,特别是要设置tx.crs_setup_version和关闭可能产生大量误报的规则(如REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example中的规则需要重命名并自定义)。我建议的流程是:先使用示例配置让WAF跑起来,然后根据日志逐步调整,最后替换为你的自定义crs-setup.conf文件。
3.4 规则文件的组织与管理
当规则越来越多时,全部写在Caddyfile里会难以维护。最佳实践是使用Include指令引入外部规则文件。
1. 包含单个文件:
directives ` Include /etc/caddy/coraza/my_custom_rules.conf `2. 包含目录下所有.conf文件:
directives ` Include /etc/caddy/coraza/rules/*.conf `3. 使用相对路径(相对于Caddy运行目录):
directives ` Include ./waf_configs/crs-setup.conf `我个人的习惯是在Caddy配置目录下建立一个coraza子目录,里面按功能存放规则文件:
/etc/caddy/ ├── Caddyfile └── coraza/ ├── crs-setup.conf # 自定义的CRS主配置 ├── exclusion-rules.conf # 针对自己应用的白名单规则 ├── custom-rules.conf # 自定义的额外规则 └── paranoia-level-2/ # 如果需要,可以按CRS的防护等级存放规则然后在Caddyfile里这样引入:
directives ` SecRuleEngine On Include /etc/caddy/coraza/crs-setup.conf Include /etc/caddy/coraza/exclusion-rules.conf Include /etc/caddy/coraza/custom-rules.conf `这种结构清晰,便于版本控制(如用Git管理),也方便在不同环境(开发、测试、生产)间同步规则。
4. 实战:集成OWASP CRS并处理拦截响应
理论说再多不如动手试。我们来完成一个最经典的场景:为反向代理的后端服务,启用完整的OWASP CRS防护,并自定义拦截页面。
4.1 准备CRS配置文件
首先,我们需要获取OWASP CRS规则集。虽然coraza-caddy插件内置了CRS,但为了灵活调优,我们最好自己下载一份。
# 创建一个工作目录 mkdir -p ~/waf-config && cd ~/waf-config # 克隆OWASP CRS仓库(建议使用稳定版本分支,如v3.3/master) git clone --branch v3.3/master https://github.com/coreruleset/coreruleset.git cd coreruleset关键文件说明:
crs-setup.conf.example: 主配置文件示例,包含所有可调参数。rules/: 目录下是所有具体的防护规则,按攻击类型分类(如SQL注入、XSS等)。rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example: 用于在CRS规则前设置白名单的示例。
创建生产环境配置:
# 复制示例配置文件并进行修改 cp crs-setup.conf.example crs-setup.conf cp rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf编辑crs-setup.conf,找到并修改以下关键配置(仅列举部分):
# 设置CRS版本,用于规则更新追踪 SecAction \ “id:900990,\ phase:1,\ nolog,\ pass,\ t:none,\ setvar:tx.crs_setup_version=343” # 设置防护等级(1-4,越高越严格,误报也可能越多) SecAction \ “id:900000,\ phase:1,\ nolog,\ pass,\ t:none,\ setvar:tx.paranoia_level=1” # 设置异常评分阈值(超过则拦截) SecAction \ “id:900000,\ phase:1,\ nolog,\ pass,\ t:none,\ setvar:tx.inbound_anomaly_score_threshold=5,\ setvar:tx.outbound_anomaly_score_threshold=4”编辑rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf,这里可以为你自己的应用添加白名单。例如,如果你的登录接口/api/login的username参数经常包含特殊字符被误报,可以添加:
SecRule REQUEST_URI “@beginsWith /api/login” \ “id:1000,\ phase:1,\ pass,\ nolog,\ ctl:ruleRemoveTargetById=942100;ARGS:username”这条规则的意思是:对于以/api/login开头的请求,移除ID为942100的规则对username参数的检查。
4.2 编写完整的Caddyfile
假设我们有一个运行在http://localhost:3000的Go Web应用,我们要用Caddy做反向代理并启用WAF。
# Caddyfile { # 全局设置:必须将coraza_waf放在处理链首位 order coraza_waf first # 全局日志,方便调试 log { output file /var/log/caddy/access.log format json } } myapp.example.com { # 启用Coraza WAF coraza_waf { load_owasp_crs directives ` SecRuleEngine On SecRequestBodyLimit 134217728 # 128MB SecRequestBodyAccess On # 包含我们准备好的配置文件 Include /path/to/your/coreruleset/crs-setup.conf Include /path/to/your/coreruleset/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf Include /path/to/your/coreruleset/rules/*.conf ` } # 使用Caddy的handle_errors指令自定义拦截响应 handle_errors { @waf_blocked { err.status_code } == 403 handle @waf_blocked { header Content-Type “text/html; charset=utf-8” header X-WAF-Blocked “true” # 你可以返回一个简单的HTML页面 respond <<HTML <!DOCTYPE html> <html> <head><title>请求被拦截</title><style>body{font-family: sans-serif; padding: 2rem;}</style></head> <body> <h1>请求被安全策略拦截</h1> <p>您的请求触发了Web应用防火墙规则。如果这是误报,请联系管理员。</p> <p>请求ID: {http.request.header.X-Request-ID}</p> </body> </html> HTML 403 } } # 反向代理到后端应用 reverse_proxy localhost:3000 { header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} header_up X-Forwarded-Proto {scheme} } # 可选的日志记录,记录被WAF拦截的请求详情 log { output file /var/log/caddy/waf_blocked.log format json { time_format “rfc3339_nano” } # 可以添加条件,只记录被WAF拦截的请求,这需要结合Coraza的审计日志功能,稍复杂 } }这个配置做了几件事:
- 全局设置了
order。 - 在站点配置中启用Coraza,并加载了我们调优过的CRS规则。
- 使用
handle_errors捕获403状态码(Coraza默认拦截状态码),返回一个友好的自定义HTML页面,而不是生硬的“403 Forbidden”。 - 将请求代理到实际的后端应用。
4.3 启动与测试
启动Caddy:
# 假设编译好的caddy二进制文件和Caddyfile在当前目录 ./caddy run --config ./Caddyfile --adapter caddyfile或者以后台服务方式运行(使用systemd):
sudo ./caddy start --config /etc/caddy/Caddyfile发送测试请求:
- 正常请求:
curl https://myapp.example.com/应该能正常返回后端应用的内容。 - 触发SQL注入规则:
curl “https://myapp.example.com/search?q=1' OR '1'='1”。由于CRS规则集包含SQL注入检测,这个请求有很大概率会被拦截,返回我们自定义的403页面,并在Caddy日志/Coraza审计日志中留下记录。 - 触发XSS规则:
curl -X POST https://myapp.example.com/comment -d “content=<script>alert(1)</script>”。
- 正常请求:
查看日志:检查Caddy的访问日志和错误日志,确认拦截请求的日志格式。Coraza的拦截日志通常会通过Caddy的标准错误输出或你配置的日志文件记录,里面包含了触发的规则ID、匹配的变量等信息,是后续调优的关键依据。
5. 高级配置与性能调优
基础功能跑通后,我们需要关注如何让WAF更高效、更准确地工作,避免对正常业务造成影响。
5.1 规则引擎模式与阶段(Phase)理解
SecRuleEngine有三种模式:
On:完全启用,检测并执行动作(如拒绝)。DetectionOnly:只检测并记录日志,不执行拒绝动作。这是上线初期的推荐模式,用于观察规则是否产生大量误报。Off:完全关闭。
规则执行的phase(阶段)决定了规则在请求/响应处理流程中的执行时机:
phase:1(REQUEST_HEADERS):请求头到达后。phase:2(REQUEST_BODY):请求体解析后。大部分输入验证规则在此阶段。phase:3(RESPONSE_HEADERS):响应头发送前。phase:4(RESPONSE_BODY):响应体发送前。用于检测响应中是否泄露敏感信息。phase:5(LOGGING):日志记录阶段。
合理设置规则的phase能提升效率。例如,一个基于URL路径的白名单规则,在phase:1就可以判断通过,无需进入更耗时的phase:2进行请求体解析。
5.2 请求体处理配置优化
对于上传文件或处理大量POST数据的应用,请求体配置至关重要。
SecRequestBodyLimit 134217728 # 最大请求体大小,128MB SecRequestBodyAccess On # 是否处理请求体,On为处理 SecRequestBodyInMemoryLimit 131072 # 请求体在内存中处理的上限,128KB,超过会写入磁盘 SecRequestBodyNoFilesLimit 1048576 # 不包含文件上传的请求体大小限制,1MB SecRequestBodyLimitAction Reject # 超过限制时的动作,Reject为拒绝 SecRule REQUEST_HEADERS:Content-Type “@rx ^multipart/form-data” \ “id:200000,phase:1,t:none,nolog,pass,ctl:requestBodyProcessor=URLENCODED|MULTIPART”调优建议:
SecRequestBodyLimit应根据业务需要设置,不宜过大,防止内存耗尽攻击。SecRequestBodyInMemoryLimit设置一个合理的值,让小的请求体在内存中快速处理,大的请求体缓存到磁盘,平衡性能和内存使用。- 明确设置
requestBodyProcessor,确保能正确解析multipart/form-data格式(文件上传)。
5.3 审计日志与调试
当规则误报或需要分析攻击时,详细的审计日志是救命稻草。Coraza支持配置审计日志引擎。
# 在directives中添加 SecAuditEngine RelevantOnly # 只记录触发相关规则的请求 SecAuditLogParts ABCDEFGHIJKZ # 记录审计日志的哪些部分,Z是完整的请求/响应体(慎用,数据量大) SecAuditLogType Serial # 日志类型,Serial是顺序写入文件 SecAuditLog /var/log/coraza/audit.log # 审计日志路径 SecAuditLogStorageDir /var/log/coraza/audit # 如果使用Concurrent类型,日志存储目录 SecDebugLog /var/log/coraza/debug.log # 调试日志,非常详细,仅调试时开启 SecDebugLogLevel 3 # 调试日志级别,0-9,数字越大越详细生产环境建议:
SecAuditEngine RelevantOnly是平衡点。SecAuditLogParts通常用ABCEFHJK就够了,包含了请求头、响应头、触发的规则等关键信息,避免记录完整的请求体(Z)以保护用户隐私和节省空间。SecDebugLog和SecDebugLogLevel仅在排查疑难杂症时临时开启,因为它会产生海量日志,严重影响性能。
5.4 性能调优要点
- 规则数量与复杂度:CRS全规则集包含数百条规则。在
paranoia_level=1时,大部分高危规则已启用。如果性能压力大,可以考虑只启用特定类型的规则(如只启用SQL注入和XSS规则),或者使用ctl:ruleRemoveById禁用已知对自身应用无害的规则。 - 白名单(排除规则):这是减少误报和提升性能最有效的手段。在
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf中,为你的API路径和参数添加精确的白名单。例如,为接受富文本的编辑器接口禁用XSS检测规则。 - 正则表达式优化:CRS规则中大量使用正则表达式。过于复杂的正则式是性能杀手。虽然我们无法修改CRS内置规则,但可以通过更新到最新版CRS来获取性能优化。同时,确保你的排除规则足够精确,避免让大量请求去匹配复杂的正则。
- 资源限制:合理设置
SecRequestBodyLimit和SecPcreMatchLimit(正则匹配限制次数),防止DoS攻击。 - 使用DetectionOnly模式观察:上线前,先在
DetectionOnly模式下运行一段时间(如一周),分析审计日志,确认没有影响业务的误报后,再切换到On模式。
6. 常见问题排查与解决方案实录
在实际部署中,你肯定会遇到各种问题。下面是我踩过的一些坑和解决方法。
6.1 编译与启动问题
问题1:编译失败,提示go: module github.com/corazawaf/coraza-caddy/v2@latest found but does not contain package
- 原因:
xcaddy在解析插件路径时可能使用了错误的模块版本或路径。 - 解决:尝试指定完整的模块路径和版本。去GitHub仓库查看最新的release tag,例如:
xcaddy build --with github.com/corazawaf/coraza-caddy/v2@v2.5.0
问题2:Caddy启动失败,报错Error during parsing: parsing directive 'coraza_waf': module not installed
- 原因:运行的Caddy二进制文件没有编译进
coraza_waf模块。 - 解决:确认你运行的
caddy命令是否是你刚刚用xcaddy编译出来的那个。使用./caddy list-modules | grep coraza检查模块列表。
6.2 规则配置与拦截问题
问题3:WAF似乎没有生效,攻击请求没有被拦截
- 排查步骤:
- 检查
order指令:这是最常见的原因。确保在全局或对应站点块的最顶部有order coraza_waf first。 - 检查
SecRuleEngine:确认设置为On或DetectionOnly。 - 检查规则加载:确认
Include路径正确,文件可读。可以在directives最前面加一条简单的测试规则,如SecRule ARGS “test” “id:999,deny,log,msg:'test rule'”,然后访问/?arg=test看是否被拦截。 - 查看日志:启动Caddy时加上
--adapter caddyfile --config ./Caddyfile --watch,在前台运行并观察标准错误输出。Coraza的解析错误和拦截日志通常会打印在这里。
- 检查
问题4:正常业务请求被误拦截(误报)
- 排查步骤:
- 获取拦截详情:当请求被拦截时,查看Caddy的日志或Coraza的审计日志。关键信息是规则ID(Rule ID),例如
[id “942100”]。 - 定位规则:根据规则ID,去OWASP CRS规则文件(在
rules/目录下)中搜索。例如,ID 942100通常在REQUEST-942-APPLICATION-ATTACK-SQLI.conf文件中。查看该规则的具体描述和匹配模式。 - 分析原因:对比你的请求参数和规则的正则表达式。常见的误报原因包括:参数中包含SQL关键字(如
SELECT、WHERE)、特殊字符编码、或某些编程语言生成的特定字符串。 - 添加白名单:在
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf中添加排除规则。例如,如果你的/api/search接口的q参数允许用户输入OR这个词,而被942100规则拦截,可以添加:SecRule REQUEST_URI “@beginsWith /api/search” \ “id:1001,\ phase:1,\ pass,\ nolog,\ ctl:ruleRemoveTargetById=942100;ARGS:q”ctl:ruleRemoveTargetById表示移除指定ID规则对特定目标的检查。
- 获取拦截详情:当请求被拦截时,查看Caddy的日志或Coraza的审计日志。关键信息是规则ID(Rule ID),例如
问题5:文件上传被拦截
- 原因:CRS规则
200003可能会拦截某些文件上传的请求头或内容。 - 解决:
- 确认你的
SecRequestBodyAccess和SecRequestBodyLimit设置足够大。 - 为文件上传接口添加白名单,禁用对
REQUEST_BODY的某些检查。例如,对于/api/upload接口:
注意:这降低了安全性,请确保你的上传接口本身有严格的文件类型和内容检查。SecRule REQUEST_URI “@streq /api/upload” \ “id:1002,\ phase:1,\ pass,\ nolog,\ ctl:ruleRemoveTargetById=200003;REQUEST_BODY”
- 确认你的
6.3 性能与日志问题
问题6:启用WAF后,服务器响应明显变慢
- 排查与解决:
- 开启DetectionOnly模式:先确认是WAF导致的延迟。
- 分析审计日志:看是否集中在某些特定规则或请求上。使用工具如
tail -f和grep分析。 - 调整规则集:降低
paranoia_level(从2或3降为1)。在crs-setup.conf中,可以按需禁用整个规则文件,例如,如果你的应用是纯API且无HTML输出,可以尝试禁用响应体检查相关的规则文件(如RESPONSE-950-*)。 - 优化排除规则:确保白名单足够精确,避免不必要的规则匹配。
- 检查正则匹配限制:
SecPcreMatchLimit设置过低可能导致复杂请求提前终止匹配,但设置过高可能消耗资源。默认值通常够用。
问题7:审计日志文件增长过快
- 解决:
- 调整
SecAuditLogParts,移除不需要的部分(如Z请求/响应体)。 - 将
SecAuditEngine设置为RelevantOnly。 - 使用日志轮转工具,如
logrotate。创建一个/etc/logrotate.d/coraza文件:/var/log/coraza/*.log { daily rotate 7 compress delaycompress missingok notifempty create 644 caddy caddy postrotate # 如果Caddy不是以服务运行,可能需要发送信号重新打开日志文件 # kill -USR1 `cat /var/run/caddy.pid 2>/dev/null` 2>/dev/null || true endscript }
- 调整
部署OWASP Coraza WAF for Caddy是一个从“可用”到“好用”的持续调优过程。初期以DetectionOnly模式运行,结合详细的审计日志,耐心地梳理和添加排除规则,是减少误报、让WAF平稳融入生产环境的关键。这套完全免费的开源方案,在提供强大安全防护的同时,也给予了我们极大的灵活性和控制力。当你看到日志中那些被成功拦截的SQL注入和XSS攻击尝试时,会觉得这一切的折腾都是值得的。
