国内云厂商对未备案域名的拦截集中在 80/443,非标端口通常不受影响。将
Cloudflare 设为 DNS
Only(灰云),让浏览器直连源站的非标端口,再用 DNS-01 签发
Let’s Encrypt 证书,即可在未备案的情况下提供可信 HTTPS。
DNS Only 不是 Cloudflare
反代:橙云(Proxied)才会转发流量,灰云只负责解析。
按应用位置分为两种形态: - 形态 A:应用与 Nginx
都在云服务器上。 - 形态 B:应用在 NAT
后的内网机器上,通过 frp 穿透。frp 基础可参考《frp内网穿透搭建教程》。
两种形态的证书与公网 Nginx 配置基本相同,操作顺序为:接入
Cloudflare → 签发证书 → 配置 Nginx(形态 B 再配 frp)→ 验证 → 确认
DNS。
一、为什么选择 DNS Only?
| 浏览器连接 |
Cloudflare 边缘 |
直连源站 |
| HTTPS 端口 |
443、2053、2083、2087、2096、8443 |
任意端口 |
| 源站证书 |
Origin CA 或自签即可 |
必须是浏览器信任的证书 |
| 源站 IP |
隐藏 |
公开 |
| 上传、流式输出、WebSocket |
受 Cloudflare 限制 |
由源站自行控制 |
| 国内访问 |
延迟可能波动 |
通常更稳定 |
灰云适合需要任意端口、流式传输或端到端控制的场景;代价是源站 IP
暴露,需要自行配置证书和防火墙。
二、目标链路
形态 A:应用在云服务器
以 https://api.example.com:8443/ 为例:
1 2 3 4 5 6 7 8 9 10 11
| flowchart TD U["浏览器<br/>https://api.example.com:8443/"] DNS["Cloudflare DNS(灰云)"] subgraph ORIGIN["云服务器"] NGINX["Nginx :8443<br/>终结 TLS"] APP["应用 · 127.0.0.1:3000"] end U --> DNS DNS --> U U -- "HTTPS" --> NGINX NGINX -- "明文 HTTP" --> APP
|
形态 B:应用在内网机器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| flowchart TD U["浏览器<br/>https://a.example.com:8443/"] DNS["Cloudflare DNS(灰云)"] subgraph VPS["云服务器"] NGINX["Nginx :8443<br/>终结 TLS,保留 Host"] FRPS["frps<br/>控制端口 27500<br/>代理端口 127.0.0.1:27022"] end subgraph LAN["内网机器(无公网 IP)"] FRPC["frpc<br/>主动外连 VPS:27500"] NGLAN["Nginx :8899<br/>按 Host 分发"] APP1["应用 A · :3000"] APP2["应用 B · :8000(WebSocket)"] end U --> DNS DNS --> U U -- "HTTPS" --> NGINX NGINX -- "回环 HTTP" --> FRPS FRPS <-- "frp 隧道(TLS)" --> FRPC FRPC --> NGLAN NGLAN --> APP1 NGLAN --> APP2
|
Nginx 到 frps 的 HTTP 仅走本机回环,frpc 到 frps 的隧道由 frp TLS
加密。
三、准备
先在当前终端设置变量(替换为自己的值),后文命令会自动展开
$DOMAIN
等变量;变量仅在当前终端有效,新窗口需重新设置。
1 2 3 4 5
| DOMAIN=example.com ORIGIN_IP=203.0.113.10 HTTPS_PORT=8443 APP_PORT=3000 FRP_PORT=27500
|
1. 接入 Cloudflare 并配置解析
打开 Cloudflare
控制台,通过 Add a site
添加主域名,再按提示在域名注册商处把 NS 修改为 Cloudflare
分配的两个服务器(官方接入教程)。等待
Zone 状态变为 Active,这是后续 DNS-01
签发证书的前提。
进入 DNS → Records,添加需要访问的子域记录(记录管理文档):
| A |
api |
${ORIGIN_IP} |
DNS Only |
形态 A |
| A |
a |
${ORIGIN_IP} |
DNS Only |
形态 B 应用 A |
| A |
c |
${ORIGIN_IP} |
DNS Only |
形态 B WebSocket 应用 |
TTL 可先使用
Auto。新部署可直接使用灰云;已有橙云记录可暂时保留,等第八节再切换。每个实际访问的子域都需要解析记录,通配符证书不会自动创建
DNS 记录。记录只填写 IP,不包含 :8443;没有公网
IPv6 就不要添加 AAAA。
2. 安装依赖
1 2
| sudo apt update sudo apt install -y nginx certbot python3-certbot-dns-cloudflare
|
3. 配置安全组
安全组与防火墙按以下范围配置:
| 8443/tcp |
Nginx HTTPS |
0.0.0.0/0 |
| 27500/tcp |
frps 控制通道(形态 B) |
仅 frpc 出口 IP;无法固定时,依靠强 token 与 TLS |
| 27022/tcp |
frps 代理端口(形态 B) |
不对公网开放,并配置 proxyBindAddr |
| 80 / 443 |
本方案不使用 |
无需放行 |
frpc 主动外连,因此内网机器无需入站规则。若使用公网 IPv6,需同步配置
Nginx 监听与 IPv6 防火墙。
四、签发通配符证书
一张证书可覆盖主域和所有一级子域,后续新增子域无需重签。
1. 创建 Cloudflare API Token
在 Cloudflare API
Tokens 页面选择 Create Token,使用 Edit
zone DNS 模板,仅授予目标 Zone 的 DNS: Edit
权限,不要使用 Global API Key。
创建凭据文件(续期仍需使用,注意保密且不要提交到仓库):
1 2 3
| sudo install -d -m 700 /root/.secrets/certbot sudo install -m 600 /dev/null /root/.secrets/certbot/cloudflare.ini sudoedit /root/.secrets/certbot/cloudflare.ini
|
写入:
1
| dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
|
2. 签发
1 2 3 4 5 6 7
| sudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \ --dns-cloudflare-propagation-seconds 60 \ --cert-name "$DOMAIN" \ -d "$DOMAIN" -d "*.${DOMAIN}" \ --email admin@example.com --agree-tos --non-interactive
|
关键参数:
$DOMAIN 是第三节定义的变量,执行时会展开为
example.com;${DOMAIN}
与其等价,花括号用于明确变量边界。
--cert-name 指定证书名称,产物位于
/etc/letsencrypt/live/${DOMAIN}/,Nginx
和续期均使用此名称。
- 两个
-d 分别覆盖 example.com 和
*.example.com,后者仅包含一级子域,不含
a.b.example.com。
- DNS-01 仅验证 DNS,不占用 80/443,证书可在橙云状态下签发;等待 60
秒是为了给 DNS 传播留时间。
五、配置公网 Nginx
两种形态的 TLS 与转发头配置相同,但 server_name
和上游地址不同。下面分别给出形态 A、B 的公网 server
块,按实际部署选用;形态 B 不能只修改形态 A 的
proxy_pass。
/etc/nginx/nginx.conf 会通过
include /etc/nginx/conf.d/*.conf;
加载该目录下的全部配置。文件名和拆分方式不限,可按站点分别创建文件;以下最小示例也可放入同一文件。
1
| sudoedit /etc/nginx/conf.d/tunnel.conf
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| map $http_upgrade $connection_upgrade { default upgrade; '' close; }
server { listen 8443 ssl default_server; ssl_reject_handshake on; }
server { listen 8443 ssl; server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 100M;
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 $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s; proxy_buffering off; proxy_request_buffering off; } }
server { listen 8443 ssl; server_name a.example.com c.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 100M;
location / { proxy_pass http://127.0.0.1:27022; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s; proxy_buffering off; proxy_request_buffering off; } }
|
其他后端可复制对应形态的 server 块,并修改
server_name、proxy_pass。X-Forwarded-For $remote_addr
会丢弃客户端伪造的 XFF;X-Forwarded-Host 保留原始 Host(含
:8443),X-Forwarded-Port
单独传递端口。应用必须按框架配置可信代理,只信任来自对应 Nginx
的转发头,不能无条件信任任意客户端提供的
X-Forwarded-*。
配置完成后加载:
1
| sudo nginx -t && sudo systemctl reload nginx
|
六、形态 B:配置 frp 隧道
1. 服务器端 frps
编辑 /etc/frp/frps.toml:
1 2 3 4 5 6 7 8 9
| bindAddr = "0.0.0.0" bindPort = 27500 auth.token = "换成强随机值,openssl rand -hex 16"
proxyBindAddr = "127.0.0.1"
transport.tls.force = true
|
创建 systemd 服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| sudo tee /etc/systemd/system/frps.service >/dev/null <<EOF [Unit] Description=FRP Server After=network.target
[Service] Type=simple ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml Restart=always RestartSec=5
[Install] WantedBy=multi-user.target EOF
sudo systemctl daemon-reload sudo systemctl enable --now frps
|
确认代理端口仅绑定回环地址:
确认第五节使用形态 B 的 server 块后,重新加载公网
Nginx。
2. 内网机器 frpc
编辑 /etc/frp/frpc.toml:
1 2 3 4 5 6 7 8 9 10 11
| serverAddr = "203.0.113.10" serverPort = 27500 auth.token = "与 frps 一致" transport.tls.enable = true
[[proxies]] name = "web-tunnel" type = "tcp" localIP = "127.0.0.1" localPort = 8899 remotePort = 27022
|
同样使用 systemd 常驻并设置 Restart=always,避免 frpc
退出导致全部站点中断。
3. 内网 Nginx 分发
编辑
/etc/nginx/conf.d/lan.conf,按域名转发到不同应用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| map $http_upgrade $connection_upgrade { default upgrade; '' close; }
server { listen 127.0.0.1:8899; server_name a.example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $http_x_real_ip; proxy_set_header X-Forwarded-For $http_x_forwarded_for; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header X-Forwarded-Host $http_x_forwarded_host; proxy_set_header X-Forwarded-Port $http_x_forwarded_port; } }
server { listen 127.0.0.1:8899; server_name c.example.com; location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $http_x_real_ip; proxy_set_header X-Forwarded-For $http_x_forwarded_for; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header X-Forwarded-Host $http_x_forwarded_host; proxy_set_header X-Forwarded-Port $http_x_forwarded_port; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s; } }
|
应用仅监听 127.0.0.1,无需开放内网机器的入站端口。内网
Nginx 也应仅通过上述可信 frp
链路接收流量;否则,直接连接它的客户端可以伪造所透传的转发头。
七、验证
请在另一台外网机器上测试,部分云环境不支持从本机访问自身公网
IP(hairpin)。以下命令通过 --resolve 绕过
DNS,直接验证源站:
1 2 3 4 5
| curl --noproxy '*' --connect-timeout 10 \ --resolve "api.example.com:8443:${ORIGIN_IP}" \ -sS -o /dev/null \ -w "HTTP=%{http_code} remote=%{remote_ip} TLS=%{ssl_verify_result}\n" \ "https://api.example.com:8443/"
|
预期 remote 为源站 IP、TLS=0,HTTP
状态符合应用预期。不要加 -k,否则会跳过证书验证。
涉及登录或重定向时,还要检查应用日志中的客户端
IP、Location、登录回调和 Secure Cookie:外部协议应为
HTTPS/WSS,且跳转地址不能丢失
:8443。并非所有框架都会自动采用
X-Forwarded-Host 和
X-Forwarded-Port,需按所用框架启用代理感知并验证。
形态 B 再检查代理端口未暴露,并按需测试 WebSocket:
1 2 3 4 5
| curl --connect-timeout 5 "http://${ORIGIN_IP}:27022/" \ && echo "❌ 27022 暴露,检查 proxyBindAddr 和安全组" \ || echo "✅ 27022 已隔离"
npx wscat -c "wss://c.example.com:8443/ws"
|
八、确认 DNS Only 并验证解析
新部署如果在第三节已创建灰云记录,无需再次切换。已有橙云记录则在 Cloudflare
控制台选择对应域名,进入 DNS → Records,将 A
记录改为 DNS only(灰云)并确认指向
${ORIGIN_IP};需要回切时可提前调短 TTL。
如果此前使用橙云,还需关闭源站“仅允许 Cloudflare
IP”的防火墙规则,以及 Authenticated Origin Pulls、Cloudflare Access
等边缘鉴权,否则直连会失败。
等待 DNS 缓存更新后,在外网机器检查解析:
1
| dig +short api.example.com @1.1.1.1
|
灰云生效后应返回 ${ORIGIN_IP},而不是 Cloudflare 的
IP。随后即可直接访问
https://api.example.com:8443/,不再需要
--resolve。
九、自动续期
创建续期后的 Nginx 重载脚本:
1 2 3 4 5 6 7 8 9
| sudo install -d -m 755 /etc/letsencrypt/renewal-hooks/deploy sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
nginx -t && systemctl reload nginx EOF sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo systemctl enable --now certbot.timer sudo certbot renew --dry-run --run-deploy-hooks
|
保留 Cloudflare 凭据供续期使用;更换 Token 后同步更新凭据文件。
十、安全清单
- 灰云会公开源站 IP:最小化安全组放行范围,并加固 SSH。
- Cloudflare Token 仅授予必要权限,妥善保管并定期轮换。
- 形态 B:frp token
使用强随机值、两端一致且不进仓库;控制端口尽量限制来源 IP。
- 形态 B:
proxyBindAddr 与安全组共同隔离
27022,并显式启用 frp TLS。
- 形态 B:frpc 是单点,配置自动重启和定期探活。
- 为
client_max_body_size
设置明确上限,避免无限上传。
参考资料