Article

Cloudflare CDN 源站防护配置

为了提升源站安全性,防止公网恶意扫描,很多站长会开启 Cloudflare 橙云代理,并配合 Authenticated Origin Pulls (AOP) 实现源站的 mTLS 校验。但在 1Panel Docker OpenResty 环境下,如何优雅地将 CA 信任、客户端验证、Cloudflare 真实 IP 识别 揉进一份 nginx.conf 中,且不破坏原有的 1Panel 站点与 WAF 配置?

这篇文章记录了我的完整配置过程。适用流量链路如下:

访客 → Cloudflare 橙云代理 → 1Panel Docker OpenResty HTTPS → 后端应用

核心概念与准备工作

什么是橙云代理与源站防护

设置

配置位置

作用

Proxied(橙云)

Cloudflare DNS

让访客优先经 Cloudflare 访问网站,默认隐藏源站 IP 地址

完全加密

Cloudflare SSL/TLS

验证源站的网站服务端证书(必须是存在且有效的证书)

区域级 AOP 与源站 mTLS 校验

Cloudflare 与 OpenResty 两端

Cloudflare 出示专用客户端证书,源站验证后才接受请求

特别说明:

  • 这里的“源站防护”指上述设置配合公网入口限制,没有一个设置可以替代所有步骤。

  • AOP 证明连接方持有受信任客户端私钥;它本身不校验 HTTP Host 与 Cloudflare Zone 的绑定,所以不可以替代服务器本身的防火墙。

  • AOP 配置完成后,务必在云服务器安全组把 HTTPS 端口只开放给 CF 的官方 IP 段。另外,测试阶段不适合一开始就把 Nginx 的验证强制设为 On,先用 Optional,不然容易导致业务宕机。

实施顺序

鉴于配置步骤略微复杂,建议按照下方的步骤,进行配置:

  • 在 DNS 设置中将网站记录开启橙云代理

  • 在 SSL/TLS → 概述 中,加密模式设为 完全 (严格)

  • 在 1Panel 网站设置中已配好有效的 HTTPS 服务端证书(Let's Encrypt 或 Cloudflare Origin CA 均可,AOP 证书不替代服务端证书)

  • 在信任的管理机生成 区域级 AOP 证书,并导入源站服务器

  • 源站 1Panel/OpenResty 全局配置加载 区域级 AOP 证书(调试阶段请使用 Optional 选项,后续确认无误可手动切换为 On)

  • 开启 Cloudflare 区域级 AOP 开关,确认有无报错

  • 确认无误,全局配置区域级 AOP 证书,收紧服务器防火墙

实施指南

前置准备

1. 打开域名控制台,进入 DNS,将网站记录设置为 Proxied/已代理,即橙云。

图1 开启橙云代理
图1 开启橙云代理

2. 进入 SSL/TLS → 概述,将加密模式设为 完全(严格)。

图2 修改加密模式
图2 修改加密模式

3. 确认 1Panel 网站已启用 HTTPS,使用域名匹配且有效的服务端证书。现有 Cloudflare Origin CA 服务端证书 或 Let's Encrypt 仍需保留;AOP 客户端证书不替代它。

特别说明:

  • Cloudflare Origin CA 服务器证书,无法脱离 Cloudflare 平台使用,不被现代浏览器信任

  • Let's Encrypt 证书,免费证书,可直接在 1Panel 面板公开申请并使用(需添加 DNS 账户 API 和 ACME 账号验证)

图3 创建 Cloudflare 源站证书
图3 创建 Cloudflare 源站证书
图4 申请 Let's Encrypt 证书
图4 申请 Let's Encrypt 证书

4. 进入 SSL/TLS → Origin Server → Authenticated Origin Pulls,检查当前配置 (相关生成教程见后文)。

特别说明:

  • 不要把 全局 AOP 开关当成 区域级 AOP,两者互相独立

  • 若当前已配置 按主机名 AOP,则它对指定主机名生效。优先级高于区域级 AOP 配置,应检查其是否使用其他证书

  • SSL/TLS 加密的 关闭 (不安全)/ 灵活 模式不适用 AOP

  • Cloudflare Tunnel 也不适用此部署方式

  • 我这里为了方便,直接签发了完整的 15 年证书。如果为了安全,应把客户端有效期设置为短日期,后续使用 CA 根证书进行更新

生成自定义 AOP 证书

在安全管理主机的 Bash 终端执行,先准备 OpenSSL 和 Python 3。

每次执行都会新建目录,并不会覆盖现有密钥。将下面代码中的 example.com 替换为实际要部署的目标根域名。

生成过程中 CA 私钥会要求设置口令(提示 Enter PEM pass phrase);客户端私钥使用无口令 PEM 供 Cloudflare 使用。

Enter pass phrase for rootca.key,这个是需要输入两次之前设置的密码,不是再设置一个新密码。

umask 077
AOP_WORK="$HOME/cf-zone-aop-$(date +%Y%m%d-%H%M%S)"
mkdir "$AOP_WORK"
cd "$AOP_WORK"
# 将 example.com 替换为目标 Zone 的域名。
AOP_ZONE='example.com'
# 按日历计算:客户端 15 年,CA 在客户端到期月份后再延长一个月。
# 处理闰年;遇到目标月没有该日时,取该月最后一天。
VALIDITY_DAYS=$(python3 - <<'PYDATES'
from datetime import datetime, timezone
import calendar
today = datetime.now(timezone.utc).date()
client_year = today.year + 15
client_end = today.replace(
year=client_year,
day=min(today.day, calendar.monthrange(client_year, today.month)[1]),
)
ca_year = client_end.year + (client_end.month == 12)
ca_month = client_end.month % 12 + 1
ca_end = client_end.replace(
year=ca_year, month=ca_month,
day=min(client_end.day, calendar.monthrange(ca_year, ca_month)[1]),
)
print((client_end - today).days, (ca_end - today).days)
PYDATES
) || { echo '有效期计算失败,停止'; exit 1; }
read -r CLIENT_DAYS CA_DAYS <<< "$VALIDITY_DAYS"
printf '客户端有效期 %s 天;CA 有效期 %s 天\n' "$CLIENT_DAYS" "$CA_DAYS"
cat > rootca.cnf <<EOF
[req]
prompt = no
distinguished_name = dn
x509_extensions = v3_ca
[dn]
CN = ${AOP_ZONE} Dedicated AOP Root CA
[v3_ca]
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
EOF
openssl genrsa -aes256 -out rootca.key 4096
openssl req -x509 -new -sha256 -days "$CA_DAYS" \
-key rootca.key -out rootca.crt -config rootca.cnf
openssl genrsa -out aop-client.key 4096
openssl req -new -sha256 -key aop-client.key \
-out aop-client.csr -subj "/CN=${AOP_ZONE} Cloudflare AOP Client"
cat > client.ext <<'EOF'
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
EOF
openssl x509 -req -sha256 -days "$CLIENT_DAYS" \
-in aop-client.csr -CA rootca.crt -CAkey rootca.key \
-CAcreateserial -out aop-client.crt -extfile client.ext
openssl verify -purpose sslclient -CAfile rootca.crt aop-client.crt
openssl x509 -in aop-client.crt -noout -subject -issuer -dates
openssl x509 -in rootca.crt -noout -fingerprint -sha256

执行以下命令,检查客户端证书和私钥匹配,两条哈希应相同:

openssl x509 -in aop-client.crt -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256
openssl pkey -in aop-client.key -pubout -outform DER | openssl dgst -sha256

将证书导入 Cloudflare AOP

进入目标域名的 SSL/TLS → 原服务器 → Authenticated Origin Pulls → 区域级 AOP → 上传证书,上传上一步生成的相关证书文件。

相关证书文件的用途说明见下表

文件

导入位置

用途

aop-client.crt

Cloudflare Zone-level Certificate

回源客户端叶证书

aop-client.key

Cloudflare Zone-level Private key

回源客户端私钥

rootca.crt

源站持久化目录,命名 ca.crt

验证客户端证书的 CA 公钥

rootca.key

加密,脱机离线保存

后续签发和续期客户端证书使用,不放入 OpenResty

图5 上传区域级 AOP 证书
图5 上传区域级 AOP 证书

查看当前容器信息

以下命令在 源站宿主机 的 Bash 终端执行,需 Docker 管理权限。容器名、路径均需替换。

# 查看当前的 Docker 容器信息
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'

# 设置变量
# 设置变量 'OR_CONTAINER' 为 OpenResty 容器名称
OR_CONTAINER='替换为实际OpenResty容器名'
# 设置变量 'NGINX_BIN' 为 容器内部 NGINX 程序的路径
NGINX_BIN='/usr/local/openresty/nginx/sbin/nginx'

# 查看容器网络模式
docker inspect "$OR_CONTAINER" --format '{{.HostConfig.NetworkMode}}'
# 查看目录和文件挂载
docker inspect "$OR_CONTAINER" --format '{{range .Mounts}}{{println .Type .Source "->" .Destination "RW=" .RW}}{{end}}'
# 查看容器启动命令
docker inspect "$OR_CONTAINER" --format '{{json .Config.Entrypoint}} {{json .Config.Cmd}}'
# 查看 NGINX 版本和编译参数
docker exec "$OR_CONTAINER" "$NGINX_BIN" -V

持久化挂载配置文件

这里按照挂载整个 conf 目录进行操作,如若只单独挂载 nginx.conf 文件,则需要在容器的编排文件中添加添加一条只读映射

OR_CONF_HOST='/替换为实际宿主机conf目录'
test -f "$OR_CONF_HOST/nginx.conf" || { echo '配置路径错误,停止'; exit 1; }
# 备份放在 conf 目录之外,避免备份自身
BACKUP_DIR="/root/openresty-aop-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -m 700 "$BACKUP_DIR"
cp -a "$OR_CONF_HOST/nginx.conf" "$BACKUP_DIR/nginx.conf"
tar -C "$OR_CONF_HOST" -czf "$BACKUP_DIR/conf-before-aop.tar.gz" .
docker exec "$OR_CONTAINER" "$NGINX_BIN" -T > "$BACKUP_DIR/nginx-before-aop.txt" 2>&1
printf '备份位置:%s\n' "$BACKUP_DIR"

导入 CA 公钥证书

如果证书在其他管理主机生成,先安全传输 rootca.crt 到源站宿主机。

ROOTCA_SOURCE='/替换为源站宿主机上的rootca.crt路径'
install -m 0644 "$ROOTCA_SOURCE" "$OR_CONF_HOST/ca.crt"
# 两次输出的 SHA-256 指纹应相同
openssl x509 -in "$ROOTCA_SOURCE" -noout -fingerprint -sha256
docker exec "$OR_CONTAINER" cat /usr/local/openresty/nginx/conf/ca.crt \
| openssl x509 -noout -fingerprint -sha256

配置 1Panel OpenResty

在 1Panel 网站列表上方的 OpenResty 设置 → 配置修改 中编辑当前生效主配置,或编辑已确认的宿主机 nginx.conf;选择一种方式完成同一次修改。不要点击“恢复默认配置”,该按钮会覆盖自定义内容。

首次粘贴前:只把下方 ssl_verify_client on; 改为 ssl_verify_client optional;。验收成功后改回 on。

# Worker 运行用户及进程数
user root;
worker_processes auto;

# 错误日志:同时输出到文件和标准输出
error_log /var/log/nginx/error.log notice;
error_log /dev/stdout notice;
pid /var/run/nginx.pid;

# 单个 worker 可打开的文件描述符上限
worker_rlimit_nofile 51200;

# 加载动态模块
include /usr/local/openresty/nginx/conf/modules-enabled/*.conf;

events {
    # Linux 事件模型
    use epoll;
    # 单个 worker 的最大连接数(包含上游连接)
    worker_connections 5120;
    # 一次接受所有待处理的新连接
    multi_accept on;
}

http {
    # MIME 类型及默认响应类型
    include mime.types;
    default_type application/octet-stream;

    # Cloudflare 可信代理 IPv4 网段:部署前及后续定期核对官方列表
    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 104.16.0.0/13;
    set_real_ip_from 104.24.0.0/14;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 131.0.72.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;

    # Cloudflare 可信代理 IPv6 网段
    set_real_ip_from 2400:cb00::/32;
    set_real_ip_from 2606:4700::/32;
    set_real_ip_from 2803:f800::/32;
    set_real_ip_from 2405:b500::/32;
    set_real_ip_from 2405:8100::/32;
    set_real_ip_from 2a06:98c0::/29;
    set_real_ip_from 2c0f:f248::/32;

    # 仅对可信代理提供的请求头执行真实 IP 替换
    real_ip_header CF-Connecting-IP;
    real_ip_recursive on;

    # remote_addr:替换后的客户端 IP;realip_remote_addr:原始连接对端 IP
    # ssl_client_verify:客户端证书验证结果;cfip:原始 CF-Connecting-IP 头
    log_format main
        '$remote_addr - $remote_user [$time_local] "$request" '
        '$status $body_bytes_sent "$http_referer" '
        '"$http_user_agent" "$http_x_forwarded_for"'
        ' peer=$realip_remote_addr aop=$ssl_client_verify'
        ' cfip="$http_cf_connecting_ip"';

    # HTTP 访问日志
    access_log /var/log/nginx/access.log main;
    access_log /dev/stdout main;

    # 隐藏响应头和错误页中的版本号
    server_tokens off;

    # 文件发送及 TCP 参数
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    # 虚拟主机名称哈希桶及客户端请求缓冲参数
    server_names_hash_bucket_size 512;
    client_header_buffer_size 32k;
    client_max_body_size 50m;

    # Keepalive 空闲超时(秒)及单个连接的请求数上限
    keepalive_timeout 60;
    keepalive_requests 5000;

    # Gzip 压缩:最小响应长度、缓冲区、最低请求协议版本及压缩等级
    gzip on;
    gzip_min_length 1k;
    gzip_buffers 4 16k;
    gzip_http_version 1.1;
    gzip_comp_level 5;

    # 额外允许压缩的 MIME 类型(text/html 默认包含)
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/ld+json
        application/javascript
        application/x-javascript
        application/xml
        application/xhtml+xml
        application/rss+xml
        application/atom+xml
        application/wasm
        image/svg+xml
        font/ttf
        font/otf;

    # 添加 Vary: Accept-Encoding,并允许压缩带 Via 头的代理请求响应
    gzip_vary on;
    gzip_proxied any;

    # 连接限制共享内存区域;实际限制需在子配置中使用 limit_conn 设置
    limit_conn_zone $binary_remote_addr zone=perip:10m;
    limit_conn_zone $server_name zone=perserver:10m;

    # HTTPS 虚拟主机默认客户端证书校验策略,server 可显式覆盖
    # CA 文件用于验证客户端证书;当前 on 表示强制校验
    # 首次部署若需观察,可先用 optional,确认日志为 SUCCESS 后再用 on
    ssl_client_certificate /usr/local/openresty/nginx/conf/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    # HTTP 虚拟主机及其他子配置
    include /usr/local/openresty/nginx/conf/http.d/*.conf;
    include /usr/local/openresty/nginx/conf/conf.d/*.conf;
    include /usr/local/openresty/nginx/conf/default/*.conf;

    # 1Panel WAF 配置
    include /usr/local/openresty/1pwaf/data/conf/waf.conf;
}

stream {
    # TCP / UDP 会话日志:对端地址、协议、状态、流量及会话时长
    log_format streamlog
        '$remote_addr[$time_local] '
        '$protocol $status $bytes_sent $bytes_received '
        '$session_time';

    access_log /var/log/nginx/stream-access.log streamlog;
    access_log /dev/stdout streamlog;

    # TCP / UDP 代理子配置
    include /usr/local/openresty/nginx/conf/stream.d/*.conf;
}

验证配置并启用

保存部署时按照 ssl_verify_client optional 进行配置,语法检查成功后重载配置。

docker exec "$OR_CONTAINER" "$NGINX_BIN" -t && \
docker exec "$OR_CONTAINER" "$NGINX_BIN" -s reload

在 Cloudflare 开启 区域级 AOP,并观察相关日志,确认相关配置生效后,修改为为 ssl_verify_client on,后续进行强制校验。

收尾阶段

收敛防火墙入口

  • 源站 HTTPS 回源端口只允许 Cloudflare 官方 IPv4 和 IPv6 网段进入(非必要情况关闭 HTTP-80 端口公开访问,HTTPS 只保留443端口)

  • 优先在云安全组或上游防火墙限制;规则必须覆盖 Docker 实际发布端口与 IPv6,不能只看宿主机表面规则

  • 后端应用端口借助 OpenResty 进行代理访问,避免 Docker 将其直接发布到公网

  • 1Panel 管理端口与 SSH 单独限制管理来源(例如:1.2.3.4/32)

  • nginx.conf 中的 set_real_ip_from 只是头信息信任配置,不是防火墙放行规则

  • AOP 在这里保护 HTTP 层的 HTTPS 站点,不保护明文 HTTP、stream 转发或独立管理端口

错误排查

现象

优先检查

CA 文件不存在

是否正确配置持久化挂载,容器路径是否正确

unknown directive real_ip_header

是否编译 Real IP 模块

缺少客户端证书

Cloudflare Zone 开关、实际 TLS 入口

客户端证书验证失败

CA 匹配、有效期、clientAuth 扩展

525 或 526

分别检查 TLS 握手与网站服务端证书,不能一律归因于 AOP

第一列仍为 Cloudflare IP

官方网段、头是否存在、server 覆盖、模块状态

其他网站或探活失效

http 层 AOP 默认策略影响范围