← All posts

用 Cloudflare Tunnel 从本地 SSH 到无公网 IP 的服务器

以下内容为 AI 生成,未实际验证。

基于 2026 年 7 月 Cloudflare 官方最新文档撰写。本教程只讲一个场景:目标服务器没有公网 IP,运行 cloudflared 主动连到 Cloudflare;你从本地开发机通过 Cloudflare 中转 SSH 过去。与这个场景无关的内容一律不涉及。参考链接统一列在文末。

你将搭出什么

  • 服务器不需要公网 IP、不需要端口转发,防火墙可以关掉全部入站。
  • 本地照常 ssh user@ssh.example.com,VSCode Remote-SSH、scp、rsync 都能直接用。
  • SSH 之前先过一道身份认证(邮箱验证码或 SSO),陌生人连你的 SSH banner 都看不到。
  • 全程免费(Cloudflare Zero Trust 免费档支持 50 个用户)。

整体数据流如下:

你的本地开发机                Cloudflare 边缘网络              目标服务器(无公网 IP)
┌──────────────────┐      ┌───────────────────────┐      ┌─────────────────────┐
│ ssh 客户端        │      │  Cloudflare Access    │      │  cloudflared 守护进程│
│ ssh user@ssh.    │      │  (校验身份:OTP/SSO) │ <==  │  主动向边缘拨出       │
│   example.com    │      └──────────▲────────────┘  4条  │  加密长连接           │
└────────┬─────────┘                 │ ① 先认证      长连接 │  QUIC/HTTP2 · 7844   │
         │ ProxyCommand               │             (出站) └──────────┬──────────┘
         ▼                            │                               │ 隧道内转发
┌──────────────────┐      ┌──────────┴────────────┐                   ▼
│ cloudflared      │ ===> │  Tunnel 边缘接入点     │ =============> ┌──────────────┐
│ access ssh       │      │  按主机名分发到        │   多路复用      │ sshd (22端口)│
│ (认证+代理)     │ 443  │  对应隧道的活跃连接     │                │ 只监听内网    │
└──────────────────┘      └───────────────────────┘                └──────────────┘

   客户端无监听端口暴露                服务器无需公网 IP / 端口转发,防火墙可全关入站

一、原理(5 分钟版)

为什么不需要公网 IP:传统内网穿透的思路是「从公网找入口把流量引进来」,Cloudflare Tunnel 反过来——服务器上的 cloudflared 进程主动向 Cloudflare 拨出加密长连接(默认 QUIC,UDP 7844 端口;UDP 被封会自动降级 HTTP/2,TCP 7844)。防火墙默认放行出站流量,所以服务器侧零改动。连接建立后通道是双向的:Cloudflare 边缘收到的 SSH 流量顺着这条已建立的连接送回你的服务器。

SSH 流量的完整路径:你执行 ssh user@ssh.example.com → ssh 根据 ~/.ssh/config 里的 ProxyCommand 调用本机的 cloudflared access ssh → 本机 cloudflared 先完成身份认证(首次弹浏览器,之后令牌缓存在本地)→ 向 Cloudflare 边缘发起 WebSocket 连接(443 端口)→ 边缘校验身份后,按主机名把流量转进服务器端 cloudflared 事先建立的隧道 → 服务器端 cloudflared 把流量代理到本机的 localhost:22(sshd)。之后就是正常的 SSH 密钥/密码登录——Access 认证和 SSH 认证是前后两道门,互不替代

你只需要记住这几个概念

概念 一句话解释
Tunnel(隧道) 一个持久的逻辑对象,有名字和 UUID;服务器上的 cloudflared 拿 token 启动后把它"激活"
Tunnel token 一串 eyJ...,创建隧道时 Dashboard 给你,cloudflared 靠它运行隧道。谁拿到谁就能跑这条隧道,当密码保管
Published application(路由) ssh.example.com 这个主机名的流量 → 转发到本机 localhost:22」的一条映射,创建时 Cloudflare 自动帮你建 DNS 记录
Access Application ssh.example.com 声明为受保护资源,挂上"谁能访问"的规则
Policy(策略) 那条规则本身:动作(Allow/Block/Bypass/Service Auth)+ 条件(比如邮箱等于你)

为什么有的东西只能在 Web UI 上配:隧道有两种管理模式。你在 Dashboard 里点出来的隧道是"远程管理"模式——路由规则存在 Cloudflare 云端,服务器上的 cloudflared 只拿 token 启动、联网拉配置,所以路由只能用 Web UI(或 API/Terraform)改,没有本地配置文件可写。另一种"本地管理"模式(CLI 创建、本地写 config.yml)官方定位为遗留/测试用途,本教程不用它。你照某些旧教程在服务器上写 /etc/cloudflared/config.yml 配 ingress 却不生效,就是因为你的隧道是 Dashboard 建的远程管理模式,那份文件会被完全忽略。

二、准备工作

  1. 一个 Cloudflare 账号 + 一个已托管在 Cloudflare 上的域名(NS 已指向 Cloudflare)。
  2. 目标服务器:sshd 正常运行,能出网(443 + 7844 端口)。
  3. 本地开发机:有 OpenSSH 客户端(Win10+ 自带)。

两端都装 cloudflared(服务器和本地开发机各一份):

# Debian/Ubuntu
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o /tmp/cf.deb
sudo dpkg -i /tmp/cf.deb

# macOS
brew install cloudflared

# Windows
winget install Cloudflare.cloudflared

三、服务器侧:创建隧道和路由(全部在 Web UI)

1. 创建隧道。 登录 Cloudflare Dashboard → Networking > Tunnels → Create a tunnel → 选 Cloudflared → 起个名字(如 home-nas)。

旧教程里的入口是「Zero Trust → Networks → Tunnels」。现在主控制台 Networking > Tunnels 和 Zero Trust 侧的 Networks > Connectors 管理的是同一批隧道,用哪个都行。

2. 在服务器上运行安装命令。 页面会给出一行命令:

sudo cloudflared service install eyJhIjoi...(一长串 token)

在目标服务器执行。它会装好 systemd 服务并写入 token。sudo systemctl status cloudflared 显示 active、Dashboard 隧道状态变为 Healthy,即隧道已通。

3. 添加 SSH 路由。 隧道详情页 → Routes 标签 → Add route → 选 Published application

  • Subdomain 填 ssh,Domain 选你的域名 → 得到 ssh.example.com
  • Service 类型选 SSH,地址填 localhost:22(sshd 在另一台内网机器就填那台的 IP:22

保存后 Cloudflare 自动创建 CNAME 记录(ssh.example.com → <UUID>.cfargotunnel.com),这就是你的 SSH 入口。

到这里隧道已经能转发 SSH 了——但任何人知道域名都能来爆破密码,所以下一步必做。

四、加身份门:Access 应用和 Policy(全部在 Web UI)

1. 确认登录方式。Zero Trust > Integrations > Identity providers。个人使用最省事的是添加 One-time PIN:访问时输入邮箱、收一个验证码即可登录,不需要接任何外部系统(也可以用 GitHub/Google 等 SSO,同一页面添加)。

2. 创建 Access 应用。 Zero Trust > Access controls > Applications → Create new application → 选 Self-hosted → public hostname 填 ssh.example.com(与上一步路由主机名严格一致)。

3. 配置 Policy。 在应用里新建策略:

  • Action = Allow
  • Include 规则:Emails = 你自己的邮箱

就这一条就够了。关键语义:Access 默认拒绝——不匹配 Allow 的人天然被挡,你不需要写"拒绝其他人"。Policy 里真正容易踩的坑只有一个:同一个规则里填多个值是 AND(同时满足)不是 OR;想"满足其一"就加多条 Include 规则。

保存后,任何指向 ssh.example.com 的流量先被 Access 拦下验明正身,通过后才进隧道。

五、本地侧:一行 ssh config

编辑本地 ~/.ssh/config(Windows 在 C:\Users\<你>\.ssh\config):

Host ssh.example.com
    ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h

cloudflared 路径因系统而异(macOS Homebrew 通常在 /opt/homebrew/bin/cloudflared,可用 which cloudflared 确认;Windows 写 exe 完整路径,含空格加引号)。

然后正常连接:

ssh ubuntu@ssh.example.com

首次连接会弹浏览器让你完成 Access 认证(收验证码或 SSO),令牌缓存后在有效期内不再弹窗,体验与直连公网服务器无异。scp、rsync、VSCode Remote-SSH 都直接可用。

六、CI/CD 场景(可选)

无人值守环境不能弹浏览器,用 Service Token

  1. Zero Trust > Access controls > Service credentials > Service Tokens 创建令牌,得到 Client ID + Client Secret。
  2. 在 Access 应用上再加一条策略:Action = Service Auth,Include = 该 Service Token(与真人的 Allow 策略并存)。注意动作必须是 Service Auth,选 Allow 会导致退回浏览器认证、CI 直接卡死
  3. CI 中连接:
cloudflared access ssh \
  --hostname ssh.example.com \
  --service-token-id "$CF_ACCESS_CLIENT_ID" \
  --service-token-secret "$CF_ACCESS_CLIENT_SECRET"

提醒:CI 里安装 cloudflared 要锁定版本,不要每次拉 latest(2026.6.0 就有 Service Token 被忽略的回归 bug,遇到请用 2026.5.1)。

七、验证与加固

  • Dashboard 隧道状态 Healthy
  • 自己能 ssh 登录;换一台未认证的设备访问 https://ssh.example.com,应被 Access 登录页拦住
  • 服务器防火墙拒绝所有入站(sudo ufw deny incoming 或安全组删光入站规则),SSH 只经隧道可达
  • sshd 保持加固:PasswordAuthentication noPermitRootLogin no——隧道和 Access 是额外的门,不替代 sshd 自身安全
  • tunnel token 没提交进 Git 仓库

八、排错速查

症状 原因与处置
Error 1033 服务器端 cloudflared 没跑。journalctl -u cloudflared -f 看日志
日志报 Failed to dial a quic connection UDP 7844 被封。放行它,或接受自动降级 HTTP/2
首次连接反复弹浏览器 Access 应用没建,或应用域名与路由主机名不一致
OTP 提示验证码 already used 企业邮件网关提前"消费"了验证码。重新请求,并把 noreply@notify.cloudflare.com 加白名单
改了本地 config.yml 不生效 远程管理隧道的路由只能在 Dashboard 改,本地配置文件会被忽略
CI 中报 banner exchange 超时 策略动作应为 Service Auth 而非 Allow;或 cloudflared 版本踩中回归,锁 2026.5.1

附:旧资料辨别(30 秒版)

这个产品改过好几次名和入口,搜资料时对不上很正常:

  • Argo Tunnel = Cloudflare Tunnel 的旧名,2021 年已免费;带这名字的教程收费描述和截图都过期
  • dash.teams.cloudflare.com / Zero Trust → Networks → Tunnels = 现在的 Networking > Tunnels(主控制台)或 Networks > Connectors(Zero Trust 侧)
  • 让你写 config.yml 配 ingress 的教程 = 本地管理模式,与 Dashboard 建的隧道不兼容
  • cloudflared access ssh-gen 短寿命证书方案 = 已标记 legacy,个人场景直接用本教程的 Access 认证即可,别碰

本教程基于撰写时(2026 年 7 月)的官方文档整理,仅供学习参考。Cloudflare 产品迭代较快,操作前请以官方最新文档为准。

参考链接

官方文档(核心依据)

官方博客与社区

社区实践与已知问题