前言最近我入手了一台 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）。整体架构flowchart LR Client["客户端 Mac(MacBook)"] Edge["Cloudflare 边缘节点(443 端口)"] Tunnel["Cloudflared(运行在 Mac mini)"] Host["被访问 Mac(Mac mini)"] Client -- "ssh / vnc经 cloudflared access" --> Edge Edge -- "加密隧道主动出站" --> Tunnel Tunnel -- "localhost:22localhost:5900" --> Host关键点：Mac mini 主动连出去到 Cloudflare 边缘建立长连接，所以根本不需要公网 IP，也不用在路由器上开任何端口。前置条件一个已托管在 Cloudflare 的域名（DNS 的 NS 已切到 Cloudflare）。被访问的 Mac mini，开启了「远程登录（SSH）」和「屏幕共享（VNC）」。一台客户端 Mac，安装了 Homebrew。总体流程flowchart TD A[安装 cloudflaredhost 端] --> B[cloudflared tunnel login] B --> C[创建 Tunnelcloudflared tunnel create] C --> D[编辑 config.yml定义 ingress 规则] D --> E[路由 DNStunnel route dns] E --> F[前台测试tunnel run] F --> G{是否正常?} G -- 是 --> H[安装为 serviceservice install] G -- 否 --> I[排查日志] I --> D H --> J[客户端配置~/.ssh/config] J --> K[ssh / vnc 连接]一、Host 端配置（Mac mini）1. 安装 cloudflaredbrew 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. 装 cloudflaredbrew install cloudflared which cloudflared # Apple Silicon: /opt/homebrew/bin/cloudflared # Intel: /usr/local/bin/cloudflared2. 编辑 ~/.ssh/configHost 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"?> Label com.user.cloudflared-vnc ProgramArguments /opt/homebrew/bin/cloudflared access tcp --hostname vnc.example.com --url localhost:5900 RunAtLoad <true/> KeepAlive <true/> 加载：launchctl load ~/Library/LaunchAgents/com.user.cloudflared-vnc.plist之后只要客户端 Mac 开机，本地 5900 就一直在转发，直接 vnc://localhost 就能连。四、踩坑实录与排查这部分是最有价值的——我配置过程中几乎每个错误都遇到了一遍。flowchart TD Err{遇到错误} Err --> A[TLS handshake failure] Err --> B[Operation timed out] Err --> C[websocket: bad handshake] Err --> D[no active connection] Err --> E[此主机名未被证书覆盖] A --> A1[证书未覆盖深层子域名→ 改一级子域名或开启 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.ymlerror parsing YAMLconfig.yml 缩进错了，重新检查Couldn't read credentials filecredentials-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。flowchart LR A[访问者] --> B{Cloudflare Access身份验证} B -- "通过" --> C[隧道] B -- "拒绝" --> D[拦截在边缘] C --> E[Mac miniSSH/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.ymltunnel: <你的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/configHost ssh.example.com ProxyCommand /opt/homebrew/bin/cloudflared access ssh --hostname %h User <Mac用户名>客户端 VNC LaunchAgent：见正文第三部分第 3 节。