为了提升源站安全性,防止公网恶意扫描,很多站长会开启 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/已代理,即橙云。

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

3. 确认 1Panel 网站已启用 HTTPS,使用域名匹配且有效的服务端证书。现有 Cloudflare Origin CA 服务端证书 或 Let's Encrypt 仍需保留;AOP 客户端证书不替代它。
特别说明:
Cloudflare Origin CA 服务器证书,无法脱离 Cloudflare 平台使用,不被现代浏览器信任
Let's Encrypt 证书,免费证书,可直接在 1Panel 面板公开申请并使用(需添加 DNS 账户 API 和 ACME 账号验证)


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 |

查看当前容器信息
以下命令在 源站宿主机 的 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 默认策略影响范围 |