痛点:暴露内网服务的老路越走越窄
运维团队经常面对一个需求:把内网的 Grafana、Jenkins、内部 API 安全暴露给远程团队访问。
传统方案:
| 方案 | 问题 |
|---|---|
| 公网 IP + Nginx 反代 | 需要开端口、配 TLS、防 DDoS,攻击面大 |
| VPN(WireGuard/OpenVPN) | 客户端配置复杂,移动端体验差,排错成本高 |
| SSH 隧道 | 临时可以,生产不稳定,断线无自动恢复 |
| frp/ngrok | 依赖中转服务器,带宽受限,安全性参差 |
核心矛盾:想让特定人访问内网服务,又不想在防火墙上开任何入站端口。
Cloudflare Tunnel(原 Argo Tunnel)正好解决这个问题——服务器只需出站连接到 Cloudflare 边缘网络,无需公网 IP、无需开端口,流量通过 Cloudflare 全球网络加速并做安全防护。
方案:Cloudflare Tunnel + Zero Trust Access
架构原理:
用户浏览器 → Cloudflare Edge (TLS终止 + WAF + Access策略)
↕ (出站长连接,无需入站)
内网服务器 cloudflared → localhost:3000 (Grafana)
关键特性: - 零入站端口:cloudflared 进程从内网主动建立出站连接 - 自动 TLS:Cloudflare 边缘处理证书,内网服务可以跑 HTTP - Zero Trust Access:支持 Google/GitHub/OIDC 认证,细粒度控制谁能访问 - 内置负载均衡:多个 cloudflared 实例自动 HA
实操步骤
第 1 步:安装 cloudflared 并认证
# Debian/Ubuntu
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared
# 登录认证(会打开浏览器授权)
cloudflared tunnel login
认证完成后,凭证存储在 ~/.cloudflared/cert.pem。
第 2 步:创建 Tunnel 并配置路由
# 创建隧道
cloudflared tunnel create ops-internal
# 输出:Created tunnel ops-internal with id a]b1c2d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# 查看隧道 ID
cloudflared tunnel list
创建配置文件 ~/.cloudflared/config.yml:
tunnel: ab1c2d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx
credentials-file: /root/.cloudflared/ab1c2d3-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
ingress:
- hostname: grafana.yourdomain.com
service: http://localhost:3000
- hostname: jenkins.yourdomain.com
service: http://localhost:8080
- hostname: api-internal.yourdomain.com
service: http://localhost:8000
# 兜底规则(必须有)
- service: http_status:404
配置 DNS 路由:
cloudflared tunnel route dns ops-internal grafana.yourdomain.com
cloudflared tunnel route dns ops-internal jenkins.yourdomain.com
第 3 步:以 systemd 服务运行并验证
# 安装为系统服务
sudo cloudflared service install
# 启动并设置开机自启
sudo systemctl enable --now cloudflared
# 验证状态
sudo systemctl status cloudflared
cloudflared tunnel info ops-internal
验证访问:
# 从外部机器测试
curl -I https://grafana.yourdomain.com
# 应返回 200,且 Server 头显示 cloudflare
加分项:配置 Zero Trust Access 策略
在 Cloudflare Zero Trust 控制台 → Access → Applications 中:
- 添加 Application,类型 Self-hosted
- 设置 Application domain 为
grafana.yourdomain.com - 添加 Policy:仅允许公司邮箱域名(如
@yourcompany.com)通过 Google OAuth 认证访问 - 启用后,未认证用户访问该域名会跳转到登录页
避坑指南
坑 1:config.yml 的 ingress 缺少兜底规则
# 错误 — cloudflared 启动报错
ingress:
- hostname: grafana.yourdomain.com
service: http://localhost:3000
# 缺少兜底!
# 正确 — 最后一条不带 hostname
ingress:
- hostname: grafana.yourdomain.com
service: http://localhost:3000
- service: http_status:404
cloudflared 强制要求最后一条 ingress 规则不带 hostname 作为兜底,否则启动直接报 The last ingress rule must match all hostnames。
坑 2:内网服务绑定了 127.0.0.1 但 cloudflared 跑在 Docker 里
如果 cloudflared 运行在容器中,localhost 指向容器自身网络。解决方案:
# 方案 A:cloudflared 容器用 host 网络
docker run --network host cloudflare/cloudflared:latest tunnel run
# 方案 B:service 地址改为宿主机 IP
ingress:
- hostname: grafana.yourdomain.com
service: http://172.17.0.1:3000 # Docker 默认网桥的宿主机 IP
坑 3:Tunnel 断线后 DNS 记录残留
删除 Tunnel 前必须先清理 DNS:
# 先删 DNS 记录
cloudflared tunnel route dns --delete ops-internal grafana.yourdomain.com
# 再删 Tunnel(强制删除会留下 CNAME 残留)
cloudflared tunnel delete ops-internal
不清理的话,域名会一直 CNAME 到已失效的 Tunnel ID,返回 1033 错误。
总结
| 对比维度 | 传统 Nginx 反代 | Cloudflare Tunnel |
|---|---|---|
| 公网 IP | 必须 | 不需要 |
| 防火墙入站规则 | 需开 80/443 | 零入站端口 |
| TLS 证书 | 自己管(Let's Encrypt) | Cloudflare 自动管理 |
| DDoS 防护 | 需另外部署 | 内置 |
| 认证鉴权 | 自建(OAuth/BasicAuth) | Zero Trust Access 内置 |
| 高可用 | 需自建多节点 + LB | 多 cloudflared 实例自动 HA |
适用场景:内网监控面板、CI/CD 系统、内部 API、开发/测试环境远程访问。
不适合:对延迟极敏感的实时交易系统(多一跳 Cloudflare 边缘),以及需要非 HTTP/TCP 协议的场景(Tunnel 支持 TCP/UDP,但配置更复杂)。
一句话总结:如果你还在为暴露内网服务而头疼防火墙规则和 TLS 证书,Cloudflare Tunnel 是目前最省心的零信任方案——装一个 cloudflared、写一个 YAML,内网服务即可安全上线。