Cloudflare AOP(Authenticated Origin Pulls,经过身份验证的源站拉取)

首先我来介绍一下大概的原理

比如我想我的网站443端口给用户提供如果我直接使用cloudflare代理

显然我的公网ip:443比如1.2.3.4:443这样就可以找到我的网站

我很担心有人用诸如爬虫或者该ip的其他行为关联导致检查443端口发现我的公网ip

然后攻击者可能会对我的各个端口扫描有的时候粗心很可能留下一些破绽

黑客有可能通过某个路径进入我的服务器(比如某个漏洞)

然后在这上面有可能会监听我的流量甚至在这时候拿到一些cookie然后登录我的回话

甚至是窃取我的密码和各种资料我想这些是完全可以实现的哎

也只有黑客才知道啥都不安全哈哈哈哈哈难绷因为总想着攻击别人所以要保护好自己

所以今天就是想开放一个端口到公网提供https然后且只能被cloudflare的cdn代理

下面开始!

本教程以一个运行在 Docker 中的网站为例,目标是让网站通过 Cloudflare 对外提供 HTTPS 访问,并使用 AOP 验证 Cloudflare 到源站的客户端证书。

预期访问链路:

注意这里我们使用vps的8443走https流量因为笔者不想走443

最终需要实现四件事:

  1. 网站通过 Cloudflare 的公网 HTTPS 地址正常访问。

  2. 源站使用独立 HTTPS 端口,示例为 8443。

  3. Nginx 验证 Cloudflare 提供的客户端证书,而不只是验证服务器证书。

  4. 外部用户不能绕过 Cloudflare,直接访问源站获取网站内容。

重要区别: Cloudflare 的 SSL/TLS Full (strict) 负责验证源站服务器证书;AOP 负责让源站验证客户端身份。两者不是同一项功能。 我们后面会有两套证书一个是源站和cdn也就是FULL(strict),一个是aop就是核心这个证书只让cloudflrae的cdn且是有自配置证书的节点访问,这样做是为了防止有人使用cloudflare的网段实现绕过直达

准备工作

在开始之前,确认已经具备以下条件:

  • 一台 Linux VPS,具有公网 IPv4 可达的网络。

  • 一个已经接入 Cloudflare 的域名,DNS 记录开启代理(橙云)。

  • 网站应用已经运行,示例为 127.0.0.1:3000。(仅内网开放通过nginx反代到8443)

  • Nginx 已安装,且可以配置独立的 HTTPS 监听端口 8443。

  • 具有修改 Cloudflare DNS、SSL/TLS 和 Origin Rules 的权限。

本文使用以下虚构配置:

项目

示例值

网站域名

www.example.com 实际的域名是example.com

源站 HTTPS 端口

8443

本地应用端口

3000

AOP 客户端证书

cf-aop-client.pem

AOP 客户端私钥

cf-aop-client.key

签发客户端证书的 CA

aop-ca.pem

所有示例路径、域名和端口都可以按实际环境调整。

确认网站应用只监听本机

先检查 Docker 容器和端口映射:

docker ps
docker port <容器名称>

理想情况下,网站应用只映射到本机回环地址,例如:

services:
  web:
    image: example/web:latest
    ports:
      - "127.0.0.1:3000:3000"

这里的镜像名称仅为示例,应该保留原有应用配置。

如果应用本身直接监听 3000,则应根据实际部署方式检查监听地址:

ss -lntp

确认应用可以在源站本机访问:

curl -I http://127.0.0.1:3000/

只要应用能够正常响应,就可以继续配置 Nginx。

一般是http200表示可以访问,400或者一些其他数字则是报错

准备 AOP 客户端证书

AOP 的核心是客户端证书验证。下面演示自建 CA 并签发客户端证书的方式。

注意: 自建证书流程适用于 Cloudflare 当前支持的自定义 AOP 证书配置。上传前应确认控制台要求的证书格式、证书链和有效期符合要求。

1. 创建证书工作目录

mkdir -p ~/aop-certs
cd ~/aop-certs
umask 077

2. 创建 CA 私钥与 CA 证书

openssl genrsa -out aop-ca.key 4096

生成 CA 证书:

openssl req -x509 -new -sha256 \
  -key aop-ca.key \
  -days 3650 \
  -out aop-ca.pem \
  -subj "/CN=Example AOP CA"

生成的文件:

  • aop-ca.key:CA 私钥,必须严格保密。

  • aop-ca.pem:CA 证书,供源站 Nginx 验证客户端证书的签发者。

3. 创建客户端证书

生成客户端私钥:

openssl genrsa -out cf-aop-client.key 2048

生成证书签名请求:

openssl req -new \
  -key cf-aop-client.key \
  -out cf-aop-client.csr \
  -subj "/CN=Cloudflare AOP Client"

创建客户端证书扩展配置:

cat > client-ext.cnf <<'EOF'
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=clientAuth
EOF

用 CA 签发客户端证书:

openssl x509 -req \
  -in cf-aop-client.csr \
  -CA aop-ca.pem \
  -CAkey aop-ca.key \
  -CAcreateserial \
  -out cf-aop-client.pem \
  -days 365 \
  -sha256 \
  -extfile client-ext.cnf

检查客户端证书:

openssl verify -CAfile aop-ca.pem cf-aop-client.pem

正常情况下应看到:

cf-aop-client.pem: OK

同时检查证书用途:

openssl x509 -in cf-aop-client.pem \
  -noout -subject -issuer -dates -ext extendedKeyUsage

输出中应包含客户端身份验证用途 TLS Web Client Authentication 或相应的 clientAuth 标识。

4. 保护私钥

本教程涉及两种私钥:

  • CA 私钥 aop-ca.key:只用于签发证书,绝不能上传到 Cloudflare 或公开分享。

  • 客户端私钥 cf-aop-client.key:与客户端证书一起配置到 Cloudflare,必须作为秘密材料妥善保管。

不要把任何 .key 文件提交到 GitHub,也不要把证书私钥贴进公开教程、工单或截图。

在 Cloudflare 配置 AOP

登录 Cloudflare 控制台,进入对应网站的 SSL/TLS 配置。

根据当前控制台提供的 AOP 设置,完成以下操作:

  1. 启用 Authenticated Origin Pulls。

  2. 如果使用自定义客户端证书Per-hostname,上传 cf-aop-client.pem 和对应的 cf-aop-client.key。

  3. 在支持按主机名配置的 AOP 设置中,将证书关联到目标网站主机名。

第三步详细说一下,填写主机名,因为我选的是针对单个子域名所以选择Per-hostname

然后比如主域名example.com 假设你的子域名是 site (site.example.com)

在这里需要你提供主机名时只要写site就好,site是我的子域名你自己看情况

且该子域名记得在DNS记录加一个A记录

name: site content:1.2.3.4(你的公网ipv4)

确认设置已保存并生效。

不同 Cloudflare 账户、套餐及配置界面的可用选项可能不同;如果当前界面不支持自定义的按主机名证书配置,不要直接假设上传已经成功。

然后进入 SSL/TLS 加密模式,选择:

Full (strict)

这要求源站提供有效、匹配主机名且受信任的服务器证书。

AOP 客户端证书与源站服务器证书用途不同,不能相互替代。

将 CA 证书部署到源站

把 aop-ca.pem 安全地复制到源站,例如:

/etc/nginx/aop/aop-ca.pem

创建目录并限制权限:

sudo install -d -m 755 /etc/nginx/aop
sudo install -m 644 aop-ca.pem /etc/nginx/aop/aop-ca.pem

这里仅部署 CA 公共证书,不部署 CA 私钥。

确认文件内容可以被 OpenSSL 正确读取:

sudo openssl x509 \
  -in /etc/nginx/aop/aop-ca.pem \
  -noout -subject -issuer -dates

如果文件读取失败,应先检查证书格式和路径,不要继续切换到强制验证模式。

配置 Nginx 监听 HTTPS 8443

以下示例假设 Nginx 已经拥有有效的源站服务器证书和私钥。它们是 Nginx 用来证明源站身份的证书,不是上一步的 AOP 客户端证书。

创建或修改对应站点的配置文件,例如:

sudo nano /etc/nginx/sites-available/example-aop

写入类似配置:

log_format aop_log
    '$time_iso8601 '
    'host="$host" '
    'status=$status '
    'verify=$ssl_client_verify '
    'subject="$ssl_client_s_dn" '
    'remote=$remote_addr';

server {
    listen 8443 ssl;
    server_name www.example.com;

    # 源站服务器证书,不是 AOP 客户端证书
    ssl_certificate     /etc/nginx/ssl/origin-fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/origin-private.key;

    # AOP 客户端证书验证的信任 CA
    ssl_client_certificate /etc/nginx/aop/aop-ca.pem;
    ssl_verify_depth 2;

    # 第一阶段:只记录验证情况,不立即强制拒绝
    ssl_verify_client optional;

    access_log /var/log/nginx/aop_access.log aop_log;

    location / {
        proxy_pass http://127.0.0.1:3000;

        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
    }
}

需要注意:

  • ssl_client_certificate 指向的是 CA 证书。

  • ssl_verify_client optional 允许没有客户端证书的连接继续建立,便于排查。

  • ssl_verify_depth 2 是示例深度设置,实际值需要与证书链结构匹配。

  • proxy_pass 的目标是本机应用地址,不能误填成外部公开地址。

  • ssl_certificate 和 ssl_certificate_key 必须换成源站已有的有效证书路径。

如果 log_format 已在其他 Nginx 配置文件中定义,不要重复定义同名格式。log_format 应放在 http 配置上下文中,而不是 server 块内部。

启用配置后先检查语法:

sudo nginx -t

只有输出表明语法检查成功,才执行:

sudo systemctl reload nginx

确认监听端口:

sudo ss -lntp | grep ':8443'

再检查本机 HTTPS 响应:

curl -k -I \
  --resolve www.example.com:8443:127.0.0.1 \
  https://www.example.com:8443/

-k 仅用于这一步的临时本机排查,它会跳过服务器证书校验,不应作为日常访问方式。这里的 HTTP 响应只能说明 Nginx 到应用的链路大致可用,不能证明 AOP 已经验证成功。

配置 Cloudflare Origin Rule

由于源站监听的是 8443,而访客通常访问标准 HTTPS 端口 443,需要让 Cloudflare 将发往源站的连接改为 8443。

在 Cloudflare 控制台中进入对应网站的 Rules → Origin Rules,创建一条规则。

示例逻辑:

设置项

示例

匹配条件

主机名等于 site.example.com 洋文就是 hostname equals site.example.com

目标端口覆盖

8443 重写到8443洋文Rewrite port to 8443

其他源站属性

保持现有配置

具体字段名称以当前控制台为准。应确认规则只匹配目标网站,不要误覆盖其他站点的源站端口。

保存后,从外部网络访问:

curl -I https://www.example.com/

如果返回 HTTP 200,说明公网访问路径已经可用;如果返回 521、522、525 或 526,则需要分别排查源站连接、超时、TLS 握手和服务器证书等问题。

此时仍不能仅凭 HTTP 200 判断 AOP 已生效。 还需要继续检查源站实际收到的客户端证书。

验证客户端证书是否真的到达源站

先查看 Nginx 日志:

sudo tail -f /var/log/nginx/aop_access.log

另开一个终端,通过 Cloudflare 公网域名发起请求:

curl -I https://www.example.com/

再回到日志窗口检查对应请求的 verify 字段。

常见结果:

日志结果

含义

SUCCESS

客户端证书通过 Nginx 验证

NONE

未提供客户端证书

FAILED:...

客户端证书验证失败,具体原因由后缀说明

还可以查看 Nginx 错误日志:

sudo tail -n 100 /var/log/nginx/error.log

重点核对以下项目:

  1. Cloudflare 的 AOP 设置是否启用。

  2. 自定义客户端证书是否正确关联到目标主机名。

  3. Cloudflare 到源站的请求是否匹配预期的主机名和 Origin Rule。

  4. Nginx 信任的 CA 是否与客户端证书的签发 CA 一致。

  5. 客户端证书是否过期、用途是否正确,以及证书链深度是否合适。

  6. 访问日志是否对应本次请求,而不是旧连接或其他站点的请求。

为什么会出现 verify=NONE?

NONE 不是验证成功,而是 Nginx 没有获得可验证的客户端证书。

即使 Cloudflare 的 AOP 开关看起来已经开启,仍可能因为自定义证书没有正确关联、请求没有匹配到相应配置、实际请求经过了另一条源站路径等原因而出现这种情况。

因此必须通过日志确认,而不能仅凭控制台开关状态推断。

确认无误后启用强制验证

只有在经过多次实际请求测试,并且确认来自 Cloudflare 的请求稳定显示 verify=SUCCESS 后,才将 Nginx 配置从:

ssl_verify_client optional;

改为:

ssl_verify_client on;

然后执行:

sudo nginx -t &&
sudo systemctl reload nginx

强制验证模式下,没有提供有效客户端证书的 TLS 客户端将无法正常访问该 Nginx 站点。

切换前应确保已经准备好回滚方案和其他管理入口。 如果配置错误,强制验证可能让网站无法访问。

限制源站端口的公网访问

AOP 验证成功后,还应限制 8443 端口的网络访问来源,使其只接受 Cloudflare 官方公布的源站代理 IP 网段。

可以从 Cloudflare 官方页面核对当前网段:

实施防火墙规则时:

  1. 先确认 SSH 管理端口仍然可访问。

  2. 保留其他网站、数据库和管理服务现有的必要访问规则。

  3. 仅对目标源站 HTTPS 端口设置 Cloudflare 来源白名单。

  4. 同时考虑 IPv4 和 IPv6。

  5. 通过外部网络重新测试网站,并确认直接访问源站公网地址的 8443 端口不能绕过 Cloudflare。

不要盲目清空现有防火墙规则,也不要直接封禁所有非 Cloudflare 流量而忽略其他正在运行的服务。

注意:防火墙白名单与 AOP 是两层不同的防护。 前者限制网络来源,后者验证客户端证书。两者配合才能更可靠地减少源站绕过风险。

完整测试清单

部署结束后,可以按下面的清单逐项验证。

部署验收清单

0/7 项

本机应用正常

本机访问 127.0.0.1:3000 能收到预期响应

Nginx HTTPS 正常

8443 端口监听正常,nginx -t 检查通过

Cloudflare 公网访问正常

通过域名访问网站能得到预期 HTTP 响应

AOP 客户端证书验证成功

实际请求对应的 Nginx 日志显示 verify=SUCCESS

强制验证已开启

确认 ssl_verify_client on 已加载生效

源站端口已限制

公网直连测试失败,Cloudflare 代理访问仍然正常

其他服务不受影响

SSH、其他站点和必要管理服务仍能正常使用

复制检查结果

常见故障排查

现象

优先排查

公网访问返回 521

源站是否运行、目标端口是否正确、防火墙是否放行

返回 522

源站连通性、网络路由和超时

返回 525

源站 TLS 握手、证书配置和协议兼容性

返回 526

Full (strict) 模式下的源站证书有效性

公网返回 200,但 verify=NONE

AOP 客户端证书是否实际发送、主机名关联及请求是否匹配正确的源站配置

verify=FAILED:...

具体验证错误、CA 信任链、证书有效期和证书用途

开启 ssl_verify_client on 后网站无法访问

回滚到 optional,重新排查客户端证书配置

已经完成

  • AOP CA 与客户端证书的准备。

  • Nginx 源站 HTTPS 配置和语法检查。

  • Nginx 配置成功重载。

  • 网站经 Cloudflare 访问能够返回 HTTP 200。

最后的安全提醒: HTTP 200 证明的是网站可以访问,不代表 AOP 已经生效;Nginx 语法检查成功也不代表客户端证书验证成功。整个部署最关键的验收证据是实际请求对应的 verify=SUCCESS,以及启用强制验证后,未经授权的客户端无法直接访问源站。

部署成功之后哈哈哈对自己的网站的网络安全又安心了一些!如果担心你的网站有天被黑客入侵记得收藏本文

以待使用哦!

最后一层保险限制网段(可选)

下面采用 UFW + 独立规则文件 + systemd 定时任务 的方案。Cloudflare 官方说明,源站可以使用官方公布的代理 IP 网段进行白名单限制,并建议阻止不属于 Cloudflare 或其他可信来源的直接访问。

1. 先明确规则设计

脚本只维护一个专用的 UFW 规则集合,目标是 TCP 8443:

  • 允许 Cloudflare IPv4 网段访问 8443/tcp。

  • 允许 Cloudflare IPv6 网段访问 8443/tcp。

  • 拒绝其他来源访问 8443/tcp。

  • 不改动 SSH、其他端口和已有的 UFW 默认策略。

  • 下载、格式校验或更新失败时,保留原有规则,不清空防火墙。

有一个重要的实现细节:UFW 的规则具有顺序。如果已有的宽泛 ALLOW 8443/tcp 规则排在前面,后加的拒绝规则可能无法达到预期效果。因此,脚本会先检查冲突规则,并采用单独的、可追踪的规则标记进行管理;部署前还要确认没有其他规则绕过它。

另外,UFW 的规则操作并不是一个完整的事务。为避免更新过程中短暂出现错误规则,下面脚本先验证所有下载数据,再执行规则更新,并在发生错误时恢复更新前保存的专用规则。

2. 安装必要工具并备份 UFW 配置

先通过 SSH 登录 VPS,确认当前防火墙状态:

sudo ufw status verbose
sudo ufw status numbered
sudo grep '^IPV6=' /etc/default/ufw

检查当前是否已经有 8443/tcp 的宽泛允许规则,以及 SSH 使用的端口。下面的脚本会修改 UFW 的底层规则文件,因此如果服务器还运行其他网站,建议先在维护窗口操作。

安装依赖:

sudo apt-get update
sudo apt-get install -y curl python3 ufw

备份原始配置:

sudo install -d -m 700 /root/ufw-backups
sudo cp -a /etc/ufw/before.rules \
  /root/ufw-backups/before.rules.initial
sudo cp -a /etc/ufw/before6.rules \
  /root/ufw-backups/before6.rules.initial
sudo cp -a /etc/default/ufw \
  /root/ufw-backups/ufw-default.initial

这一步只备份文件,不会更改当前防火墙规则。

3. 创建自动更新脚本

脚本采用 Cloudflare 官方的两个纯文本数据源,并在修改配置前检查:

  • HTTP 下载是否成功。

  • IPv4、IPv6 是否都能获取。

  • 网段是否为合法 CIDR,地址族是否正确。

  • 是否存在重复、空数据或异常数量。

  • 是否已经存在同名托管规则块。

  • 修改后 ufw reload 是否成功。

创建脚本:

sudo nano /usr/local/sbin/update-cloudflare-ufw.sh

将以下完整内容粘贴进去。


#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

# Cloudflare-only ingress for TCP/8443.
# Default mode: validate and preview only.
# Apply changes with: --apply

V4_URL="https://www.cloudflare.com/ips-v4"
V6_URL="https://www.cloudflare.com/ips-v6"
PORT=8443

RULES4="/etc/ufw/before.rules"
RULES6="/etc/ufw/before6.rules"
STATE="/var/lib/cloudflare-ufw"
LOG="/var/log/update-cloudflare-ufw.log"
LOCK="/run/lock/update-cloudflare-ufw.lock"

MODE="${1:---check}"
if [[ "$MODE" != "--check" && "$MODE" != "--apply" ]]; then
  echo "Usage: $0 [--check|--apply]" >&2
  exit 2
fi

exec 9>"$LOCK"
flock -n 9 || {
  echo "Another update is running." >&2
  exit 1
}

mkdir -p "$STATE"
chmod 700 "$STATE"
touch "$LOG"
chmod 600 "$LOG"

log() {
  printf '[%s] %s\n' "$(date -Is)" "$*" | tee -a "$LOG"
}

TMP="$(mktemp -d)"
BACKUP=""
cleanup() { rm -rf "$TMP"; }
trap cleanup EXIT

rollback() {
  if [[ -n "$BACKUP" && -d "$BACKUP" ]]; then
    log "ERROR: restoring the previous UFW files"
    cp -a "$BACKUP/before.rules" "$RULES4"
    cp -a "$BACKUP/before6.rules" "$RULES6"
    if ! ufw reload; then
      log "CRITICAL: automatic reload after rollback failed"
      log "Check UFW manually before closing your management session"
    fi
  fi
}

on_error() {
  local rc=$?
  trap - ERR
  if [[ "$MODE" == "--apply" ]]; then
    rollback || true
  fi
  log "ERROR: update failed (exit=$rc); existing configuration should be retained or restored"
  exit "$rc"
}
trap on_error ERR

log "Downloading official Cloudflare IP lists"

curl --fail --silent --show-error --location \
  --connect-timeout 10 --max-time 30 \
  "$V4_URL" -o "$TMP/ips-v4"

curl --fail --silent --show-error --location \
  --connect-timeout 10 --max-time 30 \
  "$V6_URL" -o "$TMP/ips-v6"

# Validate both files before touching firewall configuration.
python3 - "$TMP/ips-v4" "$TMP/ips-v6" <<'PY'
import ipaddress, pathlib, sys

limits = [(4, 10, 40), (6, 5, 40)]
for filename, (version, minimum, maximum) in zip(sys.argv[1:], limits):
    raw = pathlib.Path(filename).read_text().splitlines()
    entries = [x.strip() for x in raw if x.strip()]
    if not minimum <= len(entries) <= maximum:
        raise SystemExit(
            f"Refusing update: IPv{version} count {len(entries)} "
            f"outside expected range {minimum}..{maximum}"
        )
    networks = []
    for line in entries:
        try:
            net = ipaddress.ip_network(line, strict=True)
        except ValueError as e:
            raise SystemExit(f"Invalid CIDR {line!r}: {e}")
        if net.version != version:
            raise SystemExit(f"Wrong address family: {line}")
        networks.append(str(net))
    if len(set(networks)) != len(networks):
        raise SystemExit(f"Duplicate IPv{version} ranges")
    pathlib.Path(filename).write_text("\n".join(networks) + "\n")
    print(f"IPv{version}: {len(networks)} valid CIDR ranges")
PY

# Require UFW's IPv6 support: otherwise IPv6 filtering is not guaranteed.
grep -Eq '^IPV6=yes$' /etc/default/ufw || {
  echo "UFW IPv6 support is not enabled; refusing to apply." >&2
  exit 1
}

# Refuse to take over files containing an ambiguous/duplicate managed block.
for f in "$RULES4" "$RULES6"; do
  [[ -f "$f" ]]
  [[ "$(grep -c '^# BEGIN CLOUDFLARE-8443-MANAGED$' "$f" || true)" -eq 0 ]]
  [[ "$(grep -c '^# END CLOUDFLARE-8443-MANAGED$' "$f" || true)" -eq 0 ]]
done

# Make sure there is no existing broad UFW allow for TCP/8443.
# This conservative check requires manual review if such a rule exists.
if ufw status 2>/dev/null | grep -Eiq \
  '8443(/tcp)?[[:space:]]+ALLOW|ALLOW.*8443(/tcp)?'; then
  echo "An existing UFW ALLOW rule mentioning 8443 was found." >&2
  echo "Review 'sudo ufw status numbered' and remove conflicting rules manually." >&2
  exit 1
fi

# Show what will be installed. No firewall files are changed in --check mode.
echo
echo "IPv4 ranges:"
cat "$TMP/ips-v4"
echo
echo "IPv6 ranges:"
cat "$TMP/ips-v6"
echo
echo "Target: TCP/$PORT; non-Cloudflare sources will be dropped."
echo "Mode: $MODE"

if [[ "$MODE" == "--check" ]]; then
  log "Validation successful; no firewall changes made"
  exit 0
fi

# Patch only the dedicated managed block in the filter table.
# Each family gets its own list and a final DROP for TCP/8443.
patch_rules() {
  local file="$1" list="$2" family="$3"
  python3 - "$file" "$list" "$family" "$PORT" <<'PY'
import pathlib, sys

path = pathlib.Path(sys.argv[1])
list_path = pathlib.Path(sys.argv[2])
family = int(sys.argv[3])
port = int(sys.argv[4])

text = path.read_text()
if "# BEGIN CLOUDFLARE-8443-MANAGED" in text or \
   "# END CLOUDFLARE-8443-MANAGED" in text:
    raise SystemExit(f"Managed block already exists in {path}; refusing to overwrite")

nets = list_path.read_text().splitlines()
lines = ["# BEGIN CLOUDFLARE-8443-MANAGED"]
for cidr in nets:
    opt = "-s" if family == 4 else "-s"
    lines.append(
        f"-A ufw-before-input -p tcp --dport {port} "
        f"{opt} {cidr} -j ACCEPT"
    )
lines.append(
    f"-A ufw-before-input -p tcp --dport {port} -j DROP"
)
lines.append("# END CLOUDFLARE-8443-MANAGED")

# Place rules in the filter table before its COMMIT line.
# Do not silently modify an unexpected file structure.
parts = text.splitlines()
start = next((i for i, line in enumerate(parts) if line.strip() == "*filter"), None)
if start is None:
    raise SystemExit(f"No *filter table in {path}")
end = next((i for i in range(start + 1, len(parts))
            if parts[i].strip() == "COMMIT"), None)
if end is None:
    raise SystemExit(f"No filter-table COMMIT in {path}")

parts[end:end] = lines
path.write_text("\n".join(parts) + "\n")
PY
}

BACKUP="$(mktemp -d /root/ufw-backups/cf-8443.XXXXXXXX)"
cp -a "$RULES4" "$BACKUP/before.rules"
cp -a "$RULES6" "$BACKUP/before6.rules"
chmod 700 "$BACKUP"

patch_rules "$RULES4" "$TMP/ips-v4" 4
patch_rules "$RULES6" "$TMP/ips-v6" 6

# Keep a record of exactly which official ranges were applied.
install -m 600 "$TMP/ips-v4" "$STATE/ips-v4"
install -m 600 "$TMP/ips-v6" "$STATE/ips-v6"

ufw reload

log "UFW reload succeeded; managed TCP/$PORT rules installed"
log "Backup directory: $BACKUP"
log "Review: sudo ufw status numbered"

这段脚本的工作方式

  • --check:只下载和验证数据,不改防火墙。

  • --apply:备份规则文件,生成 IPv4/IPv6 规则块,然后重新加载 UFW。

  • 失败时:尽可能恢复修改前的规则文件,并尝试重新加载。

  • 自动维护的规则标记为 CLOUDFLARE-8443-MANAGED,便于检查。

这里特意把拒绝规则放在 UFW 的 before.rules / before6.rules 的 ufw-before-input 链内。它的处理顺序早于常规用户规则,因此不会依赖简单地把一个 ufw deny 追加到末尾。

重要限制: 这份脚本是保守的配置管理示例。首次应用前必须确认 UFW 文件结构符合常规 Ubuntu 配置,并审阅当前已有的自定义规则;不要直接在生产服务器上跳过检查执行。

4. 修正脚本,使它可以重复运行

在配置定时任务前,有一个必须处理的细节:自动更新脚本需要是幂等的,即同一份网段连续运行多次,不应重复添加规则。

上面的初始版本对已有托管标记采取了拒绝覆盖策略,因此第二次运行会失败。请在正式安装前,将脚本中对应的两处检查修正。

4.1 替换托管标记检查

在脚本中找到检查 BEGIN CLOUDFLARE-8443-MANAGED 的循环,将它替换为:

for f in "$RULES4" "$RULES6"; do
  [[ -f "$f" ]]
  begin_count="$(grep -c '^# BEGIN CLOUDFLARE-8443-MANAGED$' "$f" || true)"
  end_count="$(grep -c '^# END CLOUDFLARE-8443-MANAGED$' "$f" || true)"

  if [[ "$begin_count" != "$end_count" ]] ||
     [[ "$begin_count" -gt 1 ]]; then
    echo "托管规则标记异常:$f" >&2
    exit 1
  fi
done

这样可以接受不存在托管规则块的首次部署,也可以接受已经有且只有一组完整标记的后续更新。

4.2 修改规则生成逻辑

在 patch_rules() 函数中的 Python 代码里,找到原先拒绝已有托管标记的判断:

if "# BEGIN CLOUDFLARE-8443-MANAGED" in text or \   "# END CLOUDFLARE-8443-MANAGED" in text:    raise SystemExit(...)

将其替换为下面的逻辑:

begin = "# BEGIN CLOUDFLARE-8443-MANAGED"end = "# END CLOUDFLARE-8443-MANAGED"if text.count(begin) != text.count(end):    raise SystemExit("Unmatched managed block markers")if text.count(begin) > 1:    raise SystemExit("Duplicate managed blocks")if text.count(begin) == 1:    lines = text.splitlines()    first = lines.index(begin)    last = lines.index(end)    if last < first:        raise SystemExit("Invalid managed block order")    del lines[first:last + 1]    text = "\n".join(lines) + "\n"

其余生成规则的代码保持不变。这样,脚本会先移除自己上一次生成的规则块,再插入经过校验的新网段,而不是不断叠加规则。

保存后先运行检查:

sudo chmod 700 /usr/local/sbin/update-cloudflare-ufw.sh

sudo /usr/local/sbin/update-cloudflare-ufw.sh --check

这一步只验证数据,不会更新防火墙。

5. 首次应用与验证

在应用前,先检查 SSH 端口仍然允许你的管理连接。如果 UFW 尚未启用,不要仅为了执行这个脚本就贸然启用它;应先单独审计全部规则。

先做语法检查:

sudo bash -n /usr/local/sbin/update-cloudflare-ufw.sh

再下载并验证官方数据:

sudo /usr/local/sbin/update-cloudflare-ufw.sh --check

只有检查通过、确认现有规则不会与之冲突后,才执行:

sudo /usr/local/sbin/update-cloudflare-ufw.sh --apply

查看 UFW 状态:

sudo ufw status verbose
sudo ufw status numbered

查看生成的规则:

sudo grep -n -A25 -B2 \
  'CLOUDFLARE-8443-MANAGED' \
  /etc/ufw/before.rules

IPv6 规则也要检查:

sudo grep -n -A25 -B2 \
  'CLOUDFLARE-8443-MANAGED' \
  /etc/ufw/before6.rules

检查日志:

sudo tail -n 50 /var/log/update-cloudflare-ufw.log

确认网站仍能通过 Cloudflare 正常访问后,再从你有权限使用的外部网络测试直接连接源站的 8443 端口。直接连接应被防火墙阻止,而经 Cloudflare 代理的请求应正常通过。

如果你有其他可信系统需要直接访问 8443,必须先把这些需求纳入规则设计,不能直接把“所有非 Cloudflare 来源都拒绝”当作适用于所有服务器的默认方案。

6. 配置 systemd 定时自动更新

手动更新成功后,创建一个 systemd 服务。

/etc/systemd/system/update-cloudflare-ufw.service:

[Unit]
Description=Update Cloudflare IP ranges in UFW
Wants=network-online.target
After=network-online.target ufw.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-cloudflare-ufw.sh --apply
User=root
Group=root

创建定时器 /etc/systemd/system/update-cloudflare-ufw.timer:

[Unit]
Description=Daily Cloudflare UFW IP range update

[Timer]
OnCalendar=daily
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target

启用定时器:

sudo systemctl daemon-reload
sudo systemctl enable --now update-cloudflare-ufw.timer

检查定时器状态:

systemctl list-timers update-cloudflare-ufw.timer

查看执行结果:

sudo systemctl status update-cloudflare-ufw.service --no-pager
sudo journalctl -u update-cloudflare-ufw.service -n 100 --no-pager

建议每日检查一次日志,确认数据下载、规则更新和 UFW 重载都成功。Cloudflare 网段不会频繁变化,但定时更新可以避免长期使用过期清单。