一切都能正常工作,直到构建运行器需要 VPN 虚拟机所用的同一个主机套接字,那时没人能解释为什么一个任务能读到网关的密钥。所有东西都装在一台机器上,因为刚好放得下;每个虚拟机都拿到宽泛的权限,因为那是让它跑起来最快的办法。结果就是一台把信任边界等同于电源线的服务器。拆分角色比听起来省事,而且从给它们命名开始。

目前共享一切的三类角色

VPN 入口、一组私有服务和构建运行器想要的东西并不相同。第一个需要公网端口和一条狭窄的私网侧网络。第二个需要持久存储和应用密钥。第三个执行别人写的代码。

把同样的权限给这三者,正是症状令人困惑的原因,因为此后它们中任何一个都能影响另外两个。一个能触达 hypervisor 套接字的构建任务,就是一个能停掉 VPN 的任务。一个有通往管理网络路由的服务虚拟机,能改掉交换机配置。问题出在权限上,进程列表只是它显形的地方。先给角色命名,每一份权限才有地方安放。

下面的草图是这种拆分的一个起点,而不是高可用性的承诺。请把它当作待验证的假设,而不是成品设计。

拆开三种角色,而不是三个进程

一台主机上的三个虚拟机给出三份权限集合和三个网络位置。它们给不出三个故障域,设计应当把这一点明说。

只有当一个虚拟机的失陷不会自动把权限交给另外两个时,这种隔离才起作用,而这是 hypervisor 的设置,不是命名约定。把一台虚拟机叫作网关,并不能阻止另一台虚拟机触达主机。去读虚拟交换机的配置,而不是主机名,然后确认每个虚拟机确实坐在你以为的位置。

host
├─ vpn-gw      10.60.10.2/24   public 51820/udp only
├─ services    10.60.0.10/24   no route to the management network
└─ ci-runner   10.60.20.2/24   outbound only, no host socket

把拓扑写下来,然后用真正掌管它的工具去验证。出现在错误网桥上的虚拟机是一个发现,而不是一个细节。先修正网络,再改任何服务配置。

让 VPN 虚拟机只打开属于自己的门

在路由器上只发布隧道端口,其余一律不开。虚拟机的管理端口属于管理通道,不该出现在公网上,hypervisor 自己的接口也一样。

私网侧的网络应当能到达远程用户所需的服务,并到此为止。应用层的认证要保持开启,因为隧道是传输,不是登录。把 VPN 虚拟机的路由表小到能用一句话描述。如果你没法这么简单地描述它,边界其实已经模糊了。

virsh net-list --all
virsh net-dumpxml lab-private
sudo nft list ruleset | grep -n -E '51820|10.60'

主机上任何不是管理所需的监听,都不该处于监听状态。这包括 hypervisor 的 API,它很容易被留在公网接口上暴露。

构建任务需要另一种边界

CI 任务运行的是仓库所能控制的命令,所以它的信任取决于谁能推送代码。shell 执行器以运行器用户在主机上执行这些命令,这是可用的最宽边界。

容器执行器更窄,而特权容器并不属于窄的那一类。让运行器待在自己的虚拟机里,拥有自己的网络范围,只给它任务所需的产物与部署访问。一次性 worker 减少保留状态,却不减少可达性或密钥暴露。这两者仍然需要明确的限制。

服务虚拟机掌管数据,所以它的备份方案才是必须能用的那一个。让仓库、上传和数据库处在同一份时间表里。去测试一次恢复,而不是读任务状态。

为三者共同的故障做准备

一台主机就是一个故障域。一次重启会同时带走隧道和每一项私有服务,而那恰恰是最需要一条进入通道的时刻。

保留一条不依赖 VPN 虚拟机的控制台通路,无论是物理控制台、带外管理接口,还是服务商控制台。存在同一块磁盘上的快照不是独立副本,跑在被保护虚拟机内部的备份也不是。然后把冷启动当作一个序列去测,而不是一张清单:主机、虚拟网络、隧道路径,以及一个在普通账户下命名的应用。

virsh list --all
systemctl is-active libvirtd
curl -sS -m 5 -o /dev/null -w '%{http_code}\n' https://10.60.0.10/
ss -ltnp | grep -E ':51820|:443'

进程启动顺序对路由和 DNS 什么也证明不了,所以要测路径,而不是单元状态。远程用户真正需要的,是那条路径。

相关文章