背景#

我的 Homelab 由两个 K3s 集群组成,通过 Cloudflare Tunnel 对外暴露服务:

集群 位置 外部子域名
homelab Proxmox 虚拟机 argocd, auth, backup, book, grafana, notify, vault
oracle-k3s Oracle Cloud home, tool, pdf, squoosh, status, rss, keep

所有流量路径如下:

Internet → Cloudflare DNS → Cloudflare Tunnel → Traefik → 各服务

上线 SSO(ZITADEL + oauth2-proxy) 之后,认证层已经有了保障。但在 Cloudflare 边缘层,始终缺乏主动防御——扫描器、恶意爬虫、暴力破解每天都在敲门,只是没被挡在门外而已。

是时候把 WAF 配置起来了。

目标#

  • 零成本:充分利用 Cloudflare 免费套餐(Free Plan)
  • 代码化:所有规则用 Terraform 管理,不手动改 Dashboard
  • 双集群覆盖:Zone 级别的规则自动保护两个 Tunnel 下的所有子域名
  • 不影响正常使用:SSO 登录、API 调用、gRPC 等正常流量不受干扰

架构:Zone 级别一次配置,两个集群同时受益#

Cloudflare WAF 规则有三个作用范围:

  • Account 级别:跨所有域名(需要 Enterprise)
  • Zone 级别:针对单个域名及其所有子域名(Free Plan 支持)
  • Route 级别:针对特定 URL 路径(通过 Page Rules)

由于两个集群都服务同一个域名 meirong.dev,Zone 级别刚好满足需求:一套规则,保护所有子域名

                    meirong.dev (Cloudflare Zone)
                           │
          ┌────────────────┴────────────────┐
          │                                 │
  homelab tunnel                    oracle-k3s tunnel
  (ce9fd9fe...)                     (bc630e77...)
          │                                 │
  argocd / auth / vault           home / tool / rss / keep
  backup / book / grafana         pdf / squoosh / status

防护策略#

Zone 安全设置(8项)#

这些是全局的 Zone 级设置,影响所有流量:

设置 说明
SSL 模式 full 源站 ↔ Cloudflare 全程加密(Tunnel 本身已加密)
最低 TLS 版本 1.2 封堵 TLS 1.0/1.1(BEAST、POODLE 等已知漏洞)
强制 HTTPS on HTTP 请求自动重定向到 HTTPS
安全等级 medium 基于 IP 信誉库对可疑访客发起质询
浏览器完整性检查 on 拦截 HTTP 头部异常的请求(爬虫、扫描器特征)
邮件混淆 on HTML 响应中隐藏邮箱地址,防止爬虫收集
盗链保护 on 阻止其他站点直接引用本站资源
机会性加密 on 支持对 HTTP 内容使用 TLS

自定义 WAF 规则(5/5,Free Plan 上限)#

免费套餐最多允许 5 条自定义 WAF 规则,我把名额分配如下:

规则 1 — 拦截 WordPress/PHP/Admin 扫描路径

自托管服务不会用到 WordPress 或 PHP,但自动化扫描器天天在找这些路径。直接封掉:

/wp-*、/xmlrpc.php、/phpmyadmin、/pma、/adminer、/cgi-bin、/wp-login、/wp-cron

规则 2 — 拦截敏感文件访问

防止配置文件、版本控制数据、服务器信息泄露:

/.env、/.git、/.svn、/.htaccess、/.htpasswd、/.DS_Store、/server-status、/server-info

规则 3 — 封锁已知漏洞扫描器 UA

这些 User-Agent 专门用于恶意探测,没有误伤正常用户的可能:

sqlmap、nikto、nmap、masscan、dirbuster、gobuster、wpscan、havij、zmeu、acunetix、nessus、qualys

规则 4 — 对高威胁评分访客发起 Managed Challenge

Cloudflare 为每个 IP 维护 0-100 的威胁评分,分数 > 14 说明该 IP 有可疑历史记录。 managed_challenge 会展示一个 JS 质询(正常浏览器无感知,通过后缓存 24 小时):

cf.threat_score gt 14  →  managed_challenge

规则 5 — 拦截非标准 HTTP 方法

只允许正常服务实际用到的请求方法,封堵 TRACECONNECT 等攻击常用方法:

允许:GET、POST、HEAD、OPTIONS、PUT、DELETE、PATCH
其余全部拦截

速率限制(1条)#

针对认证端点的暴力破解防护,覆盖 ZITADEL、Grafana、Vault 等所有服务的登录路径:

匹配路径 阈值 封锁时长
/login/oauth2/api/login/signin/v1/auth 10次 / 10秒 / IP 10秒

Free Plan 限制:速率限制的 periodmitigation_timeout 只能设为 10 秒。 Pro Plan($20/月)可以设置 60秒窗口、600秒封锁,暴力破解防护效果会好得多。

Terraform 实现#

所有配置集中在 cloudflare/terraform/waf.tf 中,与 DNS/Tunnel 配置(main.tf)分离,便于维护。

项目结构#

cloudflare/terraform/
├── main.tf          # Tunnel 配置 + DNS 记录
├── waf.tf           # WAF 规则 + Zone 安全设置  ← 新增
├── provider.tf      # Cloudflare Provider
├── variables.tf     # 变量定义
├── terraform.tfvars # 实际值(gitignore)
└── justfile         # just plan / just apply

Zone 安全设置#

使用 cloudflare_zone_setting 资源,每个设置独立一个资源,便于追踪变更:

resource "cloudflare_zone_setting" "ssl" {
  zone_id    = data.cloudflare_zone.meirong.id
  setting_id = "ssl"
  value      = "full"
}

resource "cloudflare_zone_setting" "min_tls_version" {
  zone_id    = data.cloudflare_zone.meirong.id
  setting_id = "min_tls_version"
  value      = "1.2"
}

resource "cloudflare_zone_setting" "always_use_https" {
  zone_id    = data.cloudflare_zone.meirong.id
  setting_id = "always_use_https"
  value      = "on"
}
# ...其余 5 条类似

自定义 WAF 规则集#

resource "cloudflare_ruleset" "waf_custom_rules" {
  zone_id     = data.cloudflare_zone.meirong.id
  name        = "Custom WAF Rules"
  description = "Custom WAF rules protecting meirong.dev services"
  kind        = "zone"
  phase       = "http_request_firewall_custom"

  rules = [
    {
      action      = "block"
      expression  = "(http.request.uri.path contains \"/wp-\") or ..."
      description = "Block WordPress/PHP/admin scanner paths"
      enabled     = true
    },
    {
      action      = "managed_challenge"
      expression  = "(cf.threat_score gt 14)"
      description = "Challenge visitors with high threat score"
      enabled     = true
    },
    # ...其余 3 条
  ]
}

速率限制规则集#

resource "cloudflare_ruleset" "rate_limiting" {
  zone_id     = data.cloudflare_zone.meirong.id
  name        = "Rate Limiting Rules"
  description = "Rate limiting rules for meirong.dev"
  kind        = "zone"
  phase       = "http_ratelimit"

  rules = [
    {
      action      = "block"
      expression  = "(http.request.uri.path contains \"/login\") or ..."
      description = "Rate limit authentication endpoints (10 req/10s per IP)"
      enabled     = true
      ratelimit = {
        # Free Plan 要求必须包含 cf.colo.id(按 PoP 节点计数)
        characteristics     = ["ip.src", "cf.colo.id"]
        period              = 10   # Free Plan 限制:只能为 10
        requests_per_period = 10
        mitigation_timeout  = 10   # Free Plan 限制:只能为 10
      }
    },
  ]
}

踩坑记录#

Terraform apply 过程中遇到了几个 API 报错,记录一下以防踩坑:

1. API Token 缺少权限#

报错

POST ".../zones/.../rulesets": 403 Forbidden
{"errors":[{"code":10000,"message":"Authentication error"}]}

原因:创建 Ruleset(自定义规则 + 速率限制)需要额外的 Token 权限。Zone DNS Edit 和 Tunnel Edit 不够用。

解决:API Token 需要添加:

  • Zone > Zone WAF > Edit
  • Zone > Zone Rulesets > Edit (部分版本需要)

2. 速率限制必须包含 cf.colo.id#

报错

'[ip.src]' is not a valid value for characteristics because
characteristics field is missing 'cf.colo.id', this is required
as ratelimiting counting is processed at colocation level only

原因:Cloudflare 的速率限制在 PoP 节点(Point of Presence)层面计数,必须把 cf.colo.id 加入 characteristics 才能正常工作。

解决

characteristics = ["ip.src", "cf.colo.id"]

3. Free Plan 的 periodmitigation_timeout 只能为 10#

报错(period 60)

not entitled to use the period 60, can only use a period among [10]

报错(mitigation_timeout 600)

not entitled to use a mitigation timeout different from 10

原因:Free Plan 速率限制只支持 period=10mitigation_timeout=10。Pro Plan 才能用更长的窗口。

解决:接受限制,使用 Free Plan 最大值:

period              = 10
requests_per_period = 10
mitigation_timeout  = 10

4. Zone Setting 无法通过 Terraform 删除#

Warning: This resource cannot be destroyed from Terraform.
If you create this resource, it will be present in the API
until manually deleted.

这只是一个 Warning,不影响 apply。Zone Setting 是全局配置,Cloudflare 不允许 API 删除(只能修改值),如需恢复默认值需要去 Dashboard 手动操作。

部署#

cd cloudflare/terraform
just plan   # 预览变更
just apply  # 应用

Apply 成功后,Cloudflare Dashboard 的 Security → WAF 页面可以看到所有规则已经生效:

terraform apply ...

cloudflare_zone_setting.ssl: Creation complete after 1s
cloudflare_zone_setting.always_use_https: Creation complete after 1s
cloudflare_zone_setting.browser_check: Creation complete after 1s
cloudflare_zone_setting.min_tls_version: Creation complete after 2s
# ...
cloudflare_ruleset.waf_custom_rules: Creation complete after 1s [id=65148ae8...]
cloudflare_ruleset.rate_limiting: Creation complete after 2s [id=b8f543d6...]

Apply complete! Resources: 10 added, 0 changed, 0 destroyed.

Free Plan vs Pro Plan#

功能 Free Pro ($20/mo)
自定义 WAF 规则 5 条 100 条
速率限制窗口 10 秒 最长 3600 秒
速率限制封锁时长 10 秒 最长 86400 秒
Managed Ruleset(SQLi/XSS/RCE)
OWASP Core Ruleset
泄露凭证检测
Bot Management 基础 高级

对于个人 Homelab,Free Plan 已经能挡住绝大多数自动化扫描和机会性攻击。真正有针对性的攻击,在 SSO(ZITADEL)层面还有一道防线。

Pro Plan 升级路径#

代码里已经预留了注释,升级后直接解注释并 just apply

# resource "cloudflare_ruleset" "managed_waf" {
#   zone_id = data.cloudflare_zone.meirong.id
#   name    = "Managed WAF Rulesets"
#   kind    = "zone"
#   phase   = "http_request_firewall_managed"
#
#   rules = [
#     {
#       action = "execute"
#       action_parameters = { id = "efb7b8c949ac4650a09736fc376e9aee" }
#       expression  = "true"
#       description = "Cloudflare Managed Ruleset (SQLi/XSS/RCE)"
#       enabled     = true
#     },
#     {
#       action = "execute"
#       action_parameters = { id = "4814384a9e5d4991b9815dcfc25d2f1f" }
#       expression  = "true"
#       description = "Cloudflare OWASP Core Ruleset"
#       enabled     = true
#     },
#   ]
# }

总结#

这次 WAF 配置覆盖了三个层次:

  1. Zone 级安全设置:TLS 版本、HTTPS 强制、浏览器完整性检查等基础加固
  2. 自定义 WAF 规则(5/5):精准拦截扫描器路径、恶意 UA、危险 HTTP 方法,对高威胁 IP 发起质询
  3. 速率限制:保护认证端点不被暴力破解

最重要的一点:所有规则都是 Zone 级别的,一次配置就同时保护了 homelab 和 oracle-k3s 两个集群下的全部子域名,不需要在每个集群单独维护。

整套配置用 Terraform 管理,版本可控,和现有的 DNS/Tunnel 配置放在同一个 cloudflare/terraform/ 目录下,just apply 即可部署。