Tailscale 与代理软件共存:手机远程 SSH 到无公网 IP 的 Mac

在外面的时候常常有这么个需求,SSH 连回家里的 MacBook Pro,看一眼 Claude Code 和 Codex 跑到哪一步了、有没有卡住在等我回话。上一篇讲 AI Agent Coding 基础工具链 的时候提过,tmux 里常年挂着几个 agent,人不在电脑前也得能随时接上。

Claude Code 和 Codex 各自都有手机 app,我没有用,主要考虑是 多接入一个客户端就多一份账号暴露面,出于账号安全的考虑这条路我没走,SSH 也很方便,而且不绑死到某一个 AI Agent 工具上,后面我如果换成其他开源的 Agent,比如 Grok Builder,也没有额外的成本。

家里的宽带没有公网 IP,这一层 Tailscale 能解决:两端装上、登录同一个账号,设备之间点对点打洞,拿一个 100.x 的地址就能连回去。

问题出在 macOS 上常年开着 Surge 增强模式,手机上开着 FlClash,它们和 Tailscale 要占用的是同一批系统资源,默认配置下同时启用会互相覆盖。而我不想每次要连回去就先手动切一次 VPN。

两边最后都配通了:两个 VPN 同时处于活动状态,代理照常分流,手机上一条命令直接 attach 到 Mac 上的 tmux。下面是具体怎么配的,以及这套配置的限制在哪。

Tailscale 与代理软件共存的整体结构:Android 侧用 work profile 隔出第二个 VpnService,macOS 侧用 userspace-networking 让出网卡
macOS 侧让 tailscaled 不使用内核网络栈,Android 侧把 Tailscale 放进独立的用户空间

冲突在哪

tailscaled 以默认模式启动时会做三件事,全都是系统级的独占资源:

  1. 向内核申请一张 TUN 虚拟网卡(utun5 之类),绑上 100.x.y.z
  2. 往路由表里写一条 100.64.0.0/10 指向这张网卡
  3. 把系统 DNS 指向 100.100.100.100,也就是 MagicDNS

代理软件的 TUN 模式(Surge 增强模式、Clash TUN)要占用的正好是同样这三样。我这台 Mac 上 Surge 占着 utun4198.18.0.1/15),默认路由已经指向它。两个进程都想做系统流量的总出口,后启动的会覆盖先启动的。

具体表现因平台而异:

平台 表现
macOS 允许同时存在多张 utun,理论上能共存。实际会在 DNS 接管、fake-ip 网段重叠、TUN 把 tailscaled 自己的 UDP 又抓回去这几处出问题
iOS 系统同一时刻只允许一个 packet tunnel 生效,无法共存
Android VpnService 在同一个用户空间内独占,启动 Clash 会让 Tailscale 收到 onRevoke(),就会自动停掉,反之亦然

Android 那一条的关键词是「同一个用户空间」,而不是「整台设备」。这是后面 Android 侧方案成立的依据。

两边的解法

macOS:让 tailscaled 不使用内核网络栈。--tun=userspace-networking 会让它在自己进程里(用户态)实现一整套 TCP / IP 协议栈(用 gVisor 的 netstack 库),在用户态收发包。于是不创建 utun 网卡、不修改路由表、不接管系统 DNS,Surge 那边感知不到它的存在。

Android:换一个用户空间。使用 Shelter 等 app 可以创建独立的 work profile,work profile 在 Android 里是独立的用户空间,有自己的 VPN 槽位和网络设置。把 Tailscale 和 SSH 客户端装进 work profile,FlClash 留在主空间,两个 VPN 各占一个槽位,可以同时开启,互不影响。

这两件事互相独立,只是恰好在两个不同层面上解决了同一类问题。

macOS:让 tailscaled 不碰系统网络栈

必须用 Homebrew 版

App Store 和官网下载的 Tailscale.app 是沙盒化的,走 NetworkExtension,接收不了命令行参数。

1
brew install tailscale

代价是没有菜单栏 UI,状态、登录、重连全部靠命令行。

不要去改 Homebrew 生成的 plist

网上(以及不少 AI 给出的答案)会让你编辑 $(brew --prefix)/opt/tailscale/homebrew.mxcl.tailscale.plist,往 ProgramArguments 里加参数,再 sudo brew services restart tailscale

在当前版本的 Homebrew 上这个做法是无效的,改了不生效:

  • brew services start 时,正因为这个文件存在,brew 反而走 file = nil 分支(Library/Homebrew/services/cli.rb:138),改成照 formula 现场重新生成内容,写到 ~/Library/LaunchAgents/(无 sudo)或 /Library/LaunchDaemons/(有 sudo)。
  • brew services stop 会把生成的那份直接删掉(cli.rb:177),所以 restart 一次就退回默认参数。

验证方式很直接:往 keg 里那个 plist 塞一个标记字符串,再看 brew 启动时实际写出来的内容,标记不在里面。

结论是绕开 brew services,用自己的 label 装一个 LaunchDaemon,brew 完全碰不到它。

自己写一个 LaunchDaemon

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
<?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>dev.xueshi.tailscaled</string>

<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/tailscale/bin/tailscaled</string>
<string>--tun=userspace-networking</string>
<string>--socks5-server=localhost:1055</string>
<string>--outbound-http-proxy-listen=localhost:1055</string>
<!-- root 的 HOME 是 /var/root,state 不能用默认值,显式指定 -->
<string>--statedir=/opt/homebrew/var/lib/tailscale</string>
</array>

<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>

<key>StandardOutPath</key>
<string>/opt/homebrew/var/log/tailscaled.log</string>
<key>StandardErrorPath</key>
<string>/opt/homebrew/var/log/tailscaled.log</string>
</dict>
</plist>

装到 /Library/LaunchDaemons/

1
2
3
4
5
sudo cp dev.xueshi.tailscaled.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/dev.xueshi.tailscaled.plist
sudo chmod 644 /Library/LaunchDaemons/dev.xueshi.tailscaled.plist
sudo launchctl bootstrap system /Library/LaunchDaemons/dev.xueshi.tailscaled.plist
sudo tailscale up # 终端会打印登录链接,浏览器授权

必须是复制而不是软链。launchd 要求 LaunchDaemon 的 plist 由 root 拥有、group 和 other 不可写,软链到家目录下的文件会被判定为不安全而拒绝加载。相应地,改了自己仓库里的那份之后要重新复制一次才生效。

验收标准:

1
2
tailscale status
ifconfig | grep 100. # 应该没有任何输出

ifconfig 这条搜不到东西才说明配对了。能搜到 100.x 说明还在用内核网络栈,跟 Surge 的冲突一个都躲不掉。

为什么跑在 root 下

userspace-networking 本身不需要 root。这里用 root 只为一件事:让控制 socket 落在默认的 /var/run/tailscaled.socket,而 /var/run 只有 root 写得进去。这样 tailscale status 直接敲就行,不用每次带 --socket

早先我用的是用户级 LaunchAgent,完全不要 root,代价是 socket 只能放在 /opt/homebrew/var/run/,于是每条命令都得写成 tailscale --socket=/opt/homebrew/var/run/tailscaled.socket …。也试过用包装脚本或者 shell 函数自动补上这个 flag,后来撤掉了:那样 tailscale 这个名字就有了歧义,敲下去的到底是官方的还是自制的,长期是个记忆负担。

跑在 root 下的代价是,改动 tailnet 配置的命令要加 sudoup / down / set / logout),只读命令(status / ip / ping / netcheck)不用。

代理端口:出站必需,入站用不上

系统路由表里既然没有 100.64.0.0/10,应用直接 ssh 100.64.3.7 是发不出去的,内核不知道该往哪送。所以要在 tailscaled 上开一个代理端口当作入口,这就是 plist 里那两行:

1
2
--socks5-server=localhost:1055
--outbound-http-proxy-listen=localhost:1055

两个参数共用同一个端口,tailscaled 会自己按握手区分是 SOCKS5 还是 HTTP 代理。

入站方向不走这个端口。 手机 SSH 进来的时候,tailscaled 在 netstack 里收下这条入站 TCP 连接,再自己去 127.0.0.1 的同一个端口建一条新连接,把两头接起来。sshd 不需要任何改动。

1
2
3
4
5
6
7
8
9
10
11
【默认模式(与 Surge 冲突)】            【userspace-networking】

ssh 进程 ssh 进程
↓ connect(100.64.3.7:22) ↓ 走 SOCKS5
内核路由表 → utunN 127.0.0.1:1055 (tailscaled)
↓ ↓ 在 gVisor netstack 里建 TCP
tailscaled 加密 tailscaled 加密
↓ ↓
物理网卡 UDP 出去 物理网卡 UDP 出去

← 占用网卡 / 路由 / DNS ← 系统层面完全无感

Surge 侧的规则 (非必须)

1
2
3
4
5
6
7
[Proxy]
Tailscale = socks5, 127.0.0.1, 1055

[Rule]
DOMAIN-SUFFIX,ts.net,Tailscale
IP-CIDR,100.64.0.0/10,Tailscale,no-resolve
PROCESS-NAME,tailscaled,DIRECT

最后那条按进程名放行的规则要单独说明。它的本意是不让 Surge 抓 tailscaled 自己发往 DERP 和对端的 UDP,避免绕一圈。

副作用是:如果这台 Mac 同时当 exit node,手机借道出去的流量也出自 tailscaled 这同一个进程,按进程名分不开,会被一起放成直连。我目前没有加这条规则,让手机借用这台 Mac 上的 Surge 分流。等哪天发现 tailscaled 自己的连接被 Surge 绕出问题,再考虑按目标地址(DERP 服务器、UDP 41641)做更精确的匹配,而不是按进程名一刀切。

网上常见的三条 DIRECT 规则,有两条在这里不适用

搜 Tailscale 和代理软件共存,经常能看到这三条:

1
2
3
DOMAIN-SUFFIX,tailscale.com,DIRECT
DOMAIN-SUFFIX,ts.net,DIRECT
IP-CIDR,100.64.0.0/10,DIRECT,no-resolve

只有第一条适用。 它让 tailscaled 的控制通道和 DERP 中继绕开代理,是有意义的,而且它按目标域名匹配,不会像 PROCESS-NAME,tailscaled,DIRECT 那样误伤 exit node 替别人转发的流量。

后两条是内核模式下的标准写法。那种模式下有 utun 网卡,DIRECT 等于「交给系统路由」,而系统路由里恰好有 100.64.0.0/10 → tailscale 的 utun,所以能通。userspace 模式下这个前提没有了。我在本机测过:

  • MagicDNS 域名解析出来是 198.18.6.22,这是 Surge 假 IP 段里的地址,说明 .ts.net 的 DNS 查询被 Surge 拦下了
  • route -n get <tailnet IP> 的出口是 utun4,也就是 Surge

两类流量都进了 Surge。这时候 DIRECT 的含义是「从物理网卡直接发出去」,而物理网络没有通往 tailnet 的路,必然失败。要让 Mac 上的普通应用透明访问 tailnet,规则的目标得指向 tailscaled 的 SOCKS5,也就是上面那份配置里的 Tailscale policy,不是 DIRECT

Clash / mihomo 的等价写法:

1
2
3
4
5
6
7
8
9
10
proxies:
- name: tailscale
type: socks5
server: 127.0.0.1
port: 1055
udp: false

rules:
- DOMAIN-SUFFIX,ts.net,tailscale
- IP-CIDR,100.64.0.0/10,tailscale,no-resolve

关掉密钥过期

管理后台 → Machines → 这台 Mac → Disable key expiry。

节点密钥默认 180 天过期,过期之后需要人工重新授权。对一台「我人不在家但要能连进去」的机器来说这一步是必做的,否则某天在外面会发现连不上,而唯一的修复方式是回家。

另外,跑在 work profile 里的如果直接登录有问题,可以直接在 Tailscale 的后台创建一个 auth key,通过 auth key 来添加 device,当然它也有同样的过期时间,也可以把过期给 disable 掉。

Android:把 Tailscale 放进独立的 work profile

2026-08-09 更新:如果你的 ROM 不支持 work profile,或者手机只需要单向访问 tailnet(比如 SSH 回家),现在有一条更省事的路线 ——FlClash 的内核 mihomo 已内置 Tailscale 出站,一个 App 就能搞定,官方客户端都不用装,见《把 Tailscale 塞进 FlClash》。代价是只有出站方向,手机不能被 tailnet 里的其他设备反向访问。

Android 这边用不了 userspace 那一招,官方客户端就是围绕 VpnService 做的,没有对应的开关。所以换个层面:既然 VpnService 只在同一个用户空间内独占,那就给它一个新的用户空间。

work profile 本来是给企业 MDM 用的,但不需要真接入什么企业管理。Shelter 这个开源 app 把自己注册成设备管理器,就能在本地开出一个 work profile。

装 Shelter

它不在 Google Play 上,三个渠道:

  • F-Droid:搜 Shelter,包名 net.typeblog.shelter,要求 Android 7.0 以上
  • 作者自建源:https://fdroid.typeblog.net/repo,是开发版,签名和 F-Droid 版不同,切换需要先卸载
  • 直接下 APK:主仓库在 gitea.angry.im/PeterCxy/Shelter,GitHub 上的 PeterCxy/Shelter 是镜像

三个前置条件,任意一条不满足都装不成:

  • ROM 没有阉割 managed provisioning。国产 ROM 里 MIUI 和澎湃是重灾区,不少版本把这块砍掉了,装之前先试,别先做别的准备工作。
  • 已经设置了锁屏密码
  • 设备上还没有其他 work profile。一台设备只能有一个,公司发的 MDM 占着的话就没有办法了。

两个 app 必须在同一个空间

创建 work profile 之后,在里面装两个 app:Tailscale,以及一个 SSH 客户端(我用的是 Termius)。安装方式二选一,在主空间长按 app 图标克隆到 work profile,或者在 work profile 的 Tab 里直接装新的。

这两个必须在同一个空间,这是整套方案的核心。work profile 的隔离是按用户空间做的,跨空间的 app 走不到对方的 VPN。SSH 客户端如果留在主空间,它的流量会走主空间的 FlClash,根本看不见 work profile 里那条 tailnet。

另外,主空间原来那份 Tailscale 要卸掉或者退出登录,否则它会继续和 FlClash 争同一个 VPN 槽位,问题原样还在。

FlClash 那几个开关

主空间的 FlClash 基本不用动,有三处值得按下面这样设:

  • 栈模式:优先 Mixed,TCP 走内核、UDP 走 gVisor。遇到连不上或者部分 app 不通就换 gVisor,遇到发烫、大文件下载慢就换 System
  • 通过 VPN service 自动路由系统所有流量:必须开,不开就只剩一个本地代理端口,大部分 app 不会走。

验证

Mac 上:

1
2
tailscale status          # 能看到手机节点
ifconfig | grep 100. # 无输出,说明 userspace 模式生效

手机上:

  1. 主空间开 FlClash,work profile 开 Tailscale
  2. 通知栏应该有两条 VPN 通知,其中一条带公文包角标,那条是 work profile 的
  3. 主空间开浏览器,确认代理仍然照常分流
  4. work profile 里的 Termius 连 Mac 的 tailnet 地址(tailscale ip -4 可查),能进去

进去之后一条命令回到工作现场:

1
ssh [email protected] -t "tmux attach -t dev || tmux new -s dev"

最关键的一步是分别在主空间和 work profile 里查一次出口 IP(随便找个查 IP 的网页,或者 curl ifconfig.me),两者不同才说明隔离真的生效了。看到两条 VPN 通知不足以说明问题。如果两边的出口 IP 一样,回去检查 ROM 对 work profile 的支持和 always-on 设置。

实测记录

macOS 侧的验证结果,2026 年 8 月 2 日,tailscale 1.98.10:

检查项 结果
有没有新增 utun 网卡 没有。前后都是 utun0–4,Surge 的 utun4 没被动
有没有 100.x 地址绑到网卡 没有
路由表里有没有 100.64.0.0/10 没有
系统 DNS 有没有被指向 100.100.100.100 没有
默认路由 仍然是 Surge 的 utun4
系统 ping 任意 tailnet IP 100% 丢包,符合预期,内核没有这条路由
tailscale ping 本机
tailscale ping 手机 通,走的是 DERP 中继而不是直连,不影响可用性
SOCKS5 连 100.x:22 通,收到 SSH-2.0-OpenSSH_10.2 横幅
curl --socks5-hostname + MagicDNS 域名
curl --socks5 + MagicDNS 域名 不通,本地解析不了,符合预期
tailscale status --json 的 Health 空数组,无告警

这批数据是在用户级 LaunchAgent 版本上测的。后来改成 root LaunchDaemon,--tun=userspace-networking 这部分行为完全没变,网卡、路由、DNS、代理都一样,变的只有 socket 位置和命令要不要加 sudo

让手机借这台 Mac 上网(exit node)

userspace 模式下可以当 exit node。官方文档 Kernel vs. netstack subnet routing & exit nodes 写明,netstack 模式跑 subnet router 和 exit node 不需要内核转发,靠 gVisor netstack 终结再重建 TCP / UDP 连接。macOS 上也不需要改 IP forwarding 的 sysctl,那是 Linux 内核模式才要的。

1
tailscale set --advertise-exit-node

这条命令不需要 sudo,socket 是 0666,本机管理员用户就能改 prefs。

然后必须去管理后台批准,否则不生效:找到这台机器 → 右侧 → Edit route settings → 打开 Use as exit node。

判断批准了没有,不用开网页:

1
tailscale status --json | grep -A3 PrimaryRoutes

AdvertiseRoutes 是本机申请广播的,PrimaryRoutes 才是控制端批准后生效的。没批准时 PrimaryRoutesnullSelf.ExitNodeOptionfalse

这里和 Surge 有一层关系值得注意。这台 Mac 的默认路由是 Surge 的 utun4,tailscaled 替手机拨出去的连接走的是本机正常网络栈,所以手机的流量会被 Surge 的规则接管,等于手机在外面也用上了这台 Mac 的代理配置。多数情况下这正是想要的效果,前提是不要加前面提到的那条 PROCESS-NAME,tailscaled,DIRECT

已知限制

隔离得了流量,隐藏不了 VPN 的存在

work profile 里的 app 确实不走主空间的 VPN,但它们仍然能检测到 VPN 存在。NetworkInterface.getNetworkInterfaces()/proc/net/ 拿到的是内核的全局视图,不按用户空间分割,主空间建的那张 tun0 在 work profile 里一样列得出来。

所以这套方案不能用来绕过 VPN 检测(某些银行 app、手游会查)。它解决的是两个 VPN 能不能同时运行,和能不能藏起来是两件事。

只有走代理的流量能进 tailnet

绕过 Surge 直连的命令行程序要自己指定代理,比如在 .zshrc 里导出 ALL_PROXY=socks5h://localhost:1055

域名要交给远端解析,用 curl --socks5-hostnamesocks5h://,不要用 --socks5 / socks5://。后者在本地解析,而系统 DNS 里已经没有 MagicDNS 了。不确定的时候直接用 100.x 地址最保险。

作为客户端使用别人的 exit node、访问 subnet route,同样只在走代理的流量里生效。

协议受限

userspace 模式下 tailscaled 是终结 TCP/UDP 连接再重新发起的。ICMP ping 是特殊处理的模拟实现,除此之外的 ICMP 流量和 TCP/UDP 以外的 IP 协议(比如 SCTP)不支持。测连通性用 tailscale ping,不要用系统 ping,后者的结果没有参考价值。

没人在场的重启回不来

这台机器开了 FileVault 全盘加密。开机流程是:上电 → 停在 FileVault 解锁界面(磁盘还是加密的,系统尚未启动)→ 有人输密码 → macOS 才正式启动 → tailscaled 在这之后、桌面出现之前启动。

所以如果重启的时候没人在电脑跟前(停电来电、系统自动更新重启、你在外面远程重启),机器会停在解锁界面等人输密码,在那之前整个系统都不运行,从手机就找不到这台 Mac,也没法远程救。这跟 Tailscale 怎么配无关,换官方 app 也一样,是全盘加密的固有行为。

要远程重启又想让它自己回来,用这条代替普通重启,只对紧接着的那一次开机有效:

1
sudo fdesetup authrestart

它先验证你的密码,把解锁密钥暂存在内存里,重启后自动解锁一次,不需要人到场。

其他

  • sshd 看到的来源 IP 是 127.0.0.1,是 userspace 模式转发的结果,基于来源 IP 的访问限制和 fail2ban 这类规则要重新考虑。这一条来自 Tailscale 官方文档,我本机没有实测。
  • MagicDNS 在 Mac 上不进系统层面,域名解析要靠代理转发,或者直接用 100.x 地址。
  • 没有菜单栏 UI,Homebrew 版本来就没有。
  • 卸载 Shelter 要按顺序:设置 → 账户 → 移除 work profile,然后 设置 → 安全 → 设备管理应用 → 解除权限,最后才卸载 app。顺序反了会连数据一起丢掉。
  • 耗电:work profile 本身的结构性开销很小,多出来的电基本都是第二条加密隧道消耗的。不用的时候在系统设置里把 work profile 暂停掉就归零了。

其他方案

方案 平台 优点 缺点
userspace-networking + SOCKS5 macOS / Linux 协议无关,官方支持,换任何代理软件都不受影响 需要 Homebrew 版,失去 GUI
work profile 隔离 Android 两个 VPN 真正并存,现有配置不用动 依赖 ROM 支持,多占内存
mihomo 内置 Tailscale 出站 mihomo 全平台(我用在 Android 的 FlClash 上) 一个 App 解决,不依赖 work profile 和 ROM 支持 仅出站,不能被反向访问;要求 mihomo ≥ v1.19.25
Surge 内置 Tailscale policy macOS / iOS 配置只有几行,iOS 上唯一可行的方案 需要 auth key,仅出站,不能被反向访问
sing-box tailscale endpoint 全平台 一个客户端解决两件事 要从 Clash 整体迁移到 sing-box
exit node 服务端分流 全平台 手机零配置 全部流量绕行,家宽上传是瓶颈,网关一挂手机直接没网
手动切换 VPN 全平台 零成本 每次都要操作

我选 userspace-networking 而不是 Surge 内置的 Tailscale policy,原因是前者协议无关:换 Clash、mihomo、sing-box,甚至完全不用代理软件都照样能用,而且是 Tailscale 官方支持的模式,不会因为某个 app 版本更新就失效。

iOS 上的方案

iOS 那个「同一时刻只有一个 packet tunnel」是系统级限制,work profile 那套思路在上面没有对应物。Surge 从 iOS 5.20.0 / Mac 6.7.0 起支持把 Tailscale 直接做成一个 proxy policy,Surge 自己以 ephemeral node 的身份加入 tailnet,不占用系统的 VPN 槽位。

1
2
3
4
5
6
7
8
9
10
[Proxy]
Tailnet = tailscale, section-name=home

[Tailscale home]
auth-key = tskey-auth-xxxxxxxx
hostname = surge-iphone

[Rule]
DOMAIN-SUFFIX,tailxxxx.ts.net,Tailnet
IP-CIDR,100.64.0.0/10,Tailnet,no-resolve

几个注意事项:

  • 不支持交互式登录,必须用后台预先生成的 auth key
  • 只处理出站,tailnet 里的其他设备没法反过来访问这台手机
  • 设了 underlying-proxy 就会强制走 DERP、禁用直连 UDP,SSH 这种交互场景保持 DIRECT
  • control-url 的匹配规则不能指回这个 policy 自己,否则死锁
  • 默认闲置 600 秒拆会话,常用的话设 idle-keepalive = 0

附:Tailscale SSH

tailscale up --ssh 可以把认证从 SSH 密钥换成 tailnet 身份加 ACL,手机上不用管密钥文件。

先澄清一个常见的误解:打洞能力跟 --ssh 没有关系。「无公网 IP 也能连」这件事来自 tailnet 本身,用系统自带的 sshd 一样能连,--ssh 换掉的只是认证层。

macOS 上有两个前提:

  • 只有 Homebrew 开源版支持,沙盒 GUI 版不支持
  • ACL 里必须用 "action": "accept",不要用 "check"。check 模式需要硬件认证,macOS CLI 版不支持,控制服务器会静默拒绝下发 SSH policy,不报错也不提示,表现是连接一直挂着。
1
2
3
4
5
6
7
8
"ssh": [
{
"action": "accept",
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"users": ["autogroup:nonroot", "root"]
}
]

我目前还是用系统 sshd,关掉了密码登录(PasswordAuthentication no),公钥是唯一入口。建议先用系统 sshd 把整条链路跑通,再考虑要不要切过来,一次只动一个变量出问题好定位。

参考