前言
最近我入手了一台 Mac mini,打算把它当作家里常驻的远程服务器:跑些长时间任务、做开发环境、当 NAS、随时从公司或外面那台 MacBook 连回来用。要实现这件事,核心问题是怎么把家里这台机器安全地暴露到公网。
传统方案有几个坑:
- 路由器端口转发:得有公网 IP,运营商现在大多 NAT,搞不定;就算能搞还要应付动态 IP、DDoS、扫描器。
- frp / ngrok:需要自己一台带公网的 VPS 当中继,多一份成本和维护。
- Tailscale / ZeroTier:好用,但每台想接入的设备都得装客户端。
- Cloudflare Tunnel:免费,不要公网 IP,自带 HTTPS 和 DDoS 防护,可以叠加 Zero Trust Access 做身份验证。本文采用这套方案。
最终目标:在外面的 MacBook 上,一条 ssh user@ssh.example.com 就能连回家里的 Mac mini,并且能随时拉起屏幕共享(VNC)。
整体架构
(MacBook)"] Edge["Cloudflare 边缘节点
(443 端口)"] Tunnel["Cloudflared
(运行在 Mac mini)"] Host["被访问 Mac
(Mac mini)"] Client -- "ssh / vnc
经 cloudflared access" --> Edge Edge -- "加密隧道
主动出站" --> Tunnel Tunnel -- "localhost:22
localhost:5900" --> Host
关键点:Mac mini 主动连出去到 Cloudflare 边缘建立长连接,所以根本不需要公网 IP,也不用在路由器上开任何端口。
前置条件
- 一个已托管在 Cloudflare 的域名(DNS 的 NS 已切到 Cloudflare)。
- 被访问的 Mac mini,开启了「远程登录(SSH)」和「屏幕共享(VNC)」。
- 一台客户端 Mac,安装了 Homebrew。
总体流程
host 端] --> B[cloudflared tunnel login] B --> C[创建 Tunnel
cloudflared tunnel create] C --> D[编辑 config.yml
定义 ingress 规则] D --> E[路由 DNS
tunnel route dns] E --> F[前台测试
tunnel run] F --> G{是否正常?} G -- 是 --> H[安装为 service
service install] G -- 否 --> I[排查日志] I --> D H --> J[客户端配置
~/.ssh/config] J --> K[ssh / vnc 连接]
一、Host 端配置(Mac mini)
1. 安装 cloudflared
brew install cloudflared2. 登录并授权
cloudflared tunnel login会自动打开浏览器,登录后选择要使用的域名进行授权。完成后会在 ~/.cloudflared/ 生成 cert.pem。
3. 创建隧道
cloudflared tunnel create macminivisit记下返回的 Tunnel UUID,并确认 ~/.cloudflared/<UUID>.json 凭证文件已生成。
4. 编辑 config.yml
新建 ~/.cloudflared/config.yml:
tunnel: 3ff7b7df-25c3-43d9-a49f-f9bc05701b9c
credentials-file: /Users/你的用户名/.cloudflared/3ff7b7df-25c3-43d9-a49f-f9bc05701b9c.json
ingress:
- hostname: ssh.example.com
service: ssh://localhost:22
- hostname: vnc.example.com
service: tcp://localhost:5900
- service: http_status:404几个容易踩的点:
credentials-file必须是绝对路径,不能写~。- VNC 的 service 是
tcp://,不是vnc://。 - 最后必须有兜底
http_status:404,否则会启动失败。 - hostname 用一级子域名(
ssh.example.com),不要用深层子域名(ssh.home.example.com),否则会撞 SSL 证书覆盖问题。
校验配置:
cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://ssh.example.com5. 在 Cloudflare 创建 DNS 路由
cloudflared tunnel route dns macminivisit ssh.example.com
cloudflared tunnel route dns macminivisit vnc.example.com去 Dashboard → DNS 确认两条记录都是:
- 类型:CNAME
- 内容:
<UUID>.cfargotunnel.com - 状态:橙色云(Proxied)
如果命令报记录冲突,去 Dashboard 手动删掉旧的 A/CNAME 再跑。
6. 前台测试
不要直接装 service,先在终端里跑:
cloudflared tunnel run macminivisit看到 4 条 Registered tunnel connection 就说明边缘连上了。这时候可以另开一个终端测试访问。
7. 安装为系统服务
确认前台能跑后:
sudo cloudflared service install
sudo launchctl start com.cloudflare.cloudflared确认连接数:
cloudflared tunnel info macminivisit
# 应该看到 4x connections重要陷阱:service install会复制config.yml到/Library/Application Support/com.cloudflare.cloudflared/,service 用的是这个副本。以后改配置要么再uninstall + install一次,要么直接改系统目录里那个文件,再重启服务。
sudo cp ~/.cloudflared/config.yml "/Library/Application Support/com.cloudflare.cloudflared/config.yml"
sudo launchctl stop com.cloudflare.cloudflared
sudo launchctl start com.cloudflare.cloudflared8. 打开 macOS 共享开关
系统设置 → 通用 → 共享:
- 远程登录 → 打开(SSH,端口 22)
- 屏幕共享 → 打开(VNC,端口 5900)
并确认下方「允许以下用户访问」包含你要用的账号。
二、SSH 客户端配置
不能直接用裸 ssh 连——Cloudflare 边缘只开 80/443,22 端口根本不通,会超时。必须用 cloudflared access ssh 把流量包成 HTTPS 走 443。
1. 装 cloudflared
brew install cloudflared
which cloudflared
# Apple Silicon: /opt/homebrew/bin/cloudflared
# Intel: /usr/local/bin/cloudflared2. 编辑 ~/.ssh/config
Host ssh.example.com
ProxyCommand /opt/homebrew/bin/cloudflared access ssh --hostname %h
User 你的Mac用户名chmod 600 ~/.ssh/config3. 验证 ProxyCommand 已生效
ssh -G ssh.example.com | grep -i proxycommand应该看到:
proxycommand /opt/homebrew/bin/cloudflared access ssh --hostname ssh.example.com如果什么都没输出,就是 Host 没匹配上,检查拼写或前面是否有 Host * 覆盖。
4. 连接
ssh user@ssh.example.com第一次连接会弹浏览器走 Cloudflare 验证(如果配了 Access 策略),之后就跟普通 SSH 一样。
三、VNC 屏幕共享配置
1. 客户端起一个本地端口转发
cloudflared access tcp --hostname vnc.example.com --url localhost:5900这条命令会一直挂在前台,保持窗口开着。如果本地 5900 已被占用(比如自己 Mac 也开了屏幕共享),换个端口比如 5901。
2. 用 Finder 连接
⌘K → 「连接服务器」→ 输入:
vnc://localhost:5900输入 Mac mini 的用户名/密码,桌面就出来了。
3.(可选)做成开机自启
在客户端 Mac 上新建 ~/Library/LaunchAgents/com.user.cloudflared-vnc.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.cloudflared-vnc</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/cloudflared</string>
<string>access</string>
<string>tcp</string>
<string>--hostname</string>
<string>vnc.example.com</string>
<string>--url</string>
<string>localhost:5900</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>加载:
launchctl load ~/Library/LaunchAgents/com.user.cloudflared-vnc.plist之后只要客户端 Mac 开机,本地 5900 就一直在转发,直接 vnc://localhost 就能连。
四、踩坑实录与排查
这部分是最有价值的——我配置过程中几乎每个错误都遇到了一遍。
→ 改一级子域名
或开启 Total TLS] B --> B1[ProxyCommand 未生效
→ 检查 ~/.ssh/config] C --> C1[ingress hostname 不匹配
→ 改 config.yml + 重启] D --> D1[cloudflared 未运行
→ 前台 run 看错误] E --> E1[Universal SSL 仅覆盖
根域名和一级子域名]
错误 1:此主机名未被证书覆盖
原因:Cloudflare 的免费 Universal SSL 默认只覆盖根域名和一级子域名(example.com 和 *.example.com),不覆盖 ssh.home.example.com 这种二级子域名。
解决:
- 首选:把 hostname 改成一级子域名(
ssh.example.com),免费立即可用。 - 次选:启用 Total TLS(SSL/TLS → Edge Certificates → Total TLS),免费给所有子域名签发证书,CA 选 Let's Encrypt 即可。
- 不要:上传自己 Let's Encrypt 证书的 Custom Certificates 功能仅限 Business / Enterprise 套餐(每月 $200+)。
错误 2:remote error: tls: handshake failure
典型场景:在客户端跑 cloudflared access ssh --hostname ssh.example.com 直接报错。
原因排查清单:
- 真的把
example.com替换成自己的域名了吗?(占位符不算) - DNS 记录是 CNAME 指向
<UUID>.cfargotunnel.com、橙色云吗? - 证书是否覆盖该子域名?(见错误 1)
- SSL/TLS 模式是否设为 Full?(不是 Off / Flexible)
用 curl -v https://ssh.example.com 直接验证:
error 1033→ 隧道没运行- HTTP 404 + cf-ray 头 → 边缘 OK,但 ingress 没匹配
- SSL 错误 → 证书问题
错误 3:websocket: bad handshake
原因:边缘找到了隧道,但隧道里的 ingress 没匹配上当前 hostname,所以返回 404,cloudflared access 拿到非 101 响应升级 websocket 失败。
最常见场景:换了域名后只更新了 DNS,没改 host 端的 config.yml,或者改了 ~/.cloudflared/config.yml 但 service 用的是 /Library/Application Support/com.cloudflare.cloudflared/config.yml 那个副本。
解决:
# 同步配置
sudo cp ~/.cloudflared/config.yml "/Library/Application Support/com.cloudflare.cloudflared/config.yml"
# 重启服务
sudo launchctl stop com.cloudflare.cloudflared
sudo launchctl start com.cloudflare.cloudflared
# 验证 ingress 匹配
cloudflared tunnel ingress rule https://ssh.example.com错误 4:Your tunnel ... does not have any active connection
原因:cloudflared 进程根本没跑起来,或跑起来后连不上 Cloudflare 边缘。
排查:前台跑一次 cloudflared tunnel run macminivisit 看真实错误:
| 错误 | 解决 |
|---|---|
Cannot determine default configuration path | 加 --config ~/.cloudflared/config.yml |
error parsing YAML | config.yml 缩进错了,重新检查 |
Couldn't read credentials file | credentials-file 路径不对,必须绝对路径 |
Unable to reach the origin service | 远程登录/屏幕共享没开 |
网络超时 / failed to connect to edge | 公司网封 UDP,加 protocol: http2 |
如果是网络问题,在 config.yml 顶部加:
protocol: http2错误 5:Operation timed out(裸 ssh 连接)
原因:客户端 SSH 直接去连 Cloudflare 边缘的 22 端口,但边缘只开 80/443,肯定超时。
解决:必须配 ~/.ssh/config 用 ProxyCommand。验证:
ssh -G ssh.example.com | grep proxycommand无输出 → Host 没匹配上,检查拼写。
五、安全加固:Cloudflare Access
把 SSH/VNC 直接挂公网,即使有强密码也是定时炸弹。强烈建议加一层 Cloudflare Access(Zero Trust 免费版每月 50 用户额度,个人用绰绰有余)。
操作:
- 进入 Cloudflare Zero Trust 控制台。
- Access → Applications → Add application → Self-hosted。
- Application domain 填
ssh.example.com。 - 添加策略:Action = Allow,规则 = Emails,填你的邮箱。
- 对
vnc.example.com重复一次(VNC 协议本身较弱,更需要这层保护)。
之后第一次连接会弹浏览器要求邮箱验证,token 缓存到本地后续无感。即使有人扫到域名,没邮箱权限也碰不到 22/5900。
身份验证} B -- "通过" --> C[隧道] B -- "拒绝" --> D[拦截在边缘] C --> E[Mac mini
SSH/VNC]
六、常用命令速查
# Host 端
cloudflared tunnel list # 列出所有隧道
cloudflared tunnel info macminivisit # 看连接数
cloudflared tunnel ingress validate # 验证 ingress 配置
cloudflared tunnel ingress rule https://x # 测试 hostname 匹配哪条规则
# Service 管理
sudo cloudflared service install
sudo cloudflared service uninstall
sudo launchctl start com.cloudflare.cloudflared
sudo launchctl stop com.cloudflare.cloudflared
sudo log stream --predicate 'process == "cloudflared"' --info
# 客户端
cloudflared access ssh --hostname ssh.example.com # 测试 SSH 隧道
cloudflared access tcp --hostname vnc.example.com --url localhost:5900 # 端口转发
cloudflared access login https://ssh.example.com # 手动认证
# SSH 调试
ssh -G ssh.example.com | grep -i proxy # 验证 ProxyCommand
ssh -vvv user@ssh.example.com # 看完整流程七、为什么标题叫"从入门到放弃"
入门容易,放弃也容易:装个 cloudflared、登录、创建 tunnel,看起来三五条命令的事。
放弃不是因为它不好用——它非常好用——而是因为中间有一连串隐蔽的坑:service 用的是另一份 config 副本、Universal SSL 只覆盖一级子域名、Cloudflare 边缘不开 22 端口、ProxyCommand 必须用绝对路径……每一个错误信息都长得不像它的真实原因,不踩一遍很难定位。
但只要把这些坑踩平了,整套方案就成了真正的"配一次用一辈子":Mac mini 7×24 在家里跑着,从地球任何角落一条 SSH 连回去,从未中断。
值得一坑。
附:完整配置清单备份
Host 端 ~/.cloudflared/config.yml
tunnel: <你的UUID>
credentials-file: /Users/<用户名>/.cloudflared/<UUID>.json
protocol: http2 # 网络封 UDP 时打开
ingress:
- hostname: ssh.example.com
service: ssh://localhost:22
- hostname: vnc.example.com
service: tcp://localhost:5900
- service: http_status:404客户端 ~/.ssh/config
Host ssh.example.com
ProxyCommand /opt/homebrew/bin/cloudflared access ssh --hostname %h
User <Mac用户名>客户端 VNC LaunchAgent:见正文第三部分第 3 节。