Kibotos

安装 node

本页介绍如何在宿主机上安装 node 并加入集群。

Ubuntu 节点的 AppArmor 前置条件

Ubuntu 的 AppArmor 用户命名空间限制 会阻止 KiboVM 创建自己的沙箱。node 包提供 kibotos-vmm profile,只作用于 node 安装的 KiboVM 可执行文件:允许它创建用户命名空间,文件与设备隔离仍由 KiboVM 自己的沙箱完成。

检查并收紧 sysctl

启用了用户命名空间限制的宿主机,还必须禁止非特权进程主动切换到 unconfined profile,否则普通程序也能借用这一授权。先查看当前值:

sysctl kernel.apparmor_restrict_unprivileged_userns \
  kernel.apparmor_restrict_unprivileged_unconfined

如果第一个值是 1,第二个值也必须是 1。第二个值为 0 时由管理员设置:

set -euo pipefail
printf '%s\n' 'kernel.apparmor_restrict_unprivileged_unconfined=1' | \
  sudo tee /etc/sysctl.d/60-kibotos-apparmor.conf >/dev/null
sudo sysctl -p /etc/sysctl.d/60-kibotos-apparmor.conf

注意:

  • 这个设置限制所有非特权进程主动切换 profile。受信程序在执行时自动附着 profile 不受影响。
  • 内核没有这个 sysctl 时,先安装发行版的安全更新。
  • 包脚本不修改全局 sysctl,也不会为了通过检查而关闭用户命名空间限制或 KiboVM 沙箱。

profile 的加载

  • profile 文件在 /usr/share/kibotos/node/apparmor/kibotos-vmm,安装时不加载。
  • node 服务启动前检查宿主机条件并加载 profile;条件不满足或解析失败时拒绝启动。
  • 停止或重启 node 服务不卸载 profile。只有卸载 node 包时才移除。
  • 内核未启用 AppArmor 时仍使用 KiboVM 自己的沙箱;无法判断内核状态时启动失败。

修改了 storage.root 时

profile 默认作用于 /var/lib/kibotos/node/installations/sha256-*/kibovm。如果在配置中修改了 storage.root,把新的根目录加入管理员维护的 local fragment:

sudo install -d -m 0755 /etc/apparmor.d/local
printf '%s\n' '@{KIBOTOS_NODE_ROOT}+=/srv/kibotos-node' | \
  sudo tee /etc/apparmor.d/local/kibotos-node-roots >/dev/null

这些目录及其上级目录必须由管理员控制,不能是普通用户可写的目录,也不能用宽泛的通配符。local fragment 在卸载包时保留,node 下次启动时重新加载。

配置

第 1 步把配置写进 /etc/kibotos/node/config.toml,在那之前先定好下面几项。全部表与键见 node 配置,本节只写那一页没有替你决定的事。

必填项

  • CPU 与内存按机器的实际容量填写,node 不会自动探测。存储池的容量不写在配置里,node 加入后在 ctrl 上设置,见设置池容量。
  • 填写 ctrl 地址,以及数据面对外公布的地址。公布的主机名必须包含在签发 node 密钥时声明的证书名称里。
  • 选定 firecracker 或 kibovm 后,需要发布与之匹配的 Platform。
  • 存储目录与 socket 目录默认是 /var/lib/kibotos/node 与 /run/kibotos/node,它们的上级目录必须允许 kibotos-vmm 账户进入。socket 目录的路径要短,不能是符号链接。启动失败时日志会指出不满足的路径或设备。
  • [storage] 下用 [[storage.pool]] 至少声明一个存储池:一个目录,装着 volumes/、retained-layers/ 与一个标记文件,Volume 与保留的可写层都放在里面。池的 path 可以等于 storage.root,这是默认示例的写法;只声明一个池时它隐式为默认池。installations/、runtime/、template-cache/、state/、pki/、network/、machine-logs/ 不属于任何池,固定留在 storage.root 下的本地磁盘;machine-logs/ 保留结束的 Instance 的控制台与 VMM 日志,见控制台与 VMM 日志。声明池之后,用 kibotos-node storage-pool init 写入标记文件,node 自己从不创建它,见NFS 上的存储池。

网络前提

  • 不需要 guest 网络时,删除整个 [host_network] 表,两个后端的 Workspace 都将使用 deny-all 策略,VM 没有网卡。node 到 guest 的连接走 vsock,不依赖网络附着。

  • [host_network].address_families 必须显式设置为 ["ipv4", "ipv6"]、["ipv4"] 或 ["ipv6"],并只配置启用的地址族的地址池与上联接口。IPv6-only 的例子:

    [host_network]
    address_families = ["ipv6"]
    ipv6_pool = "2001:db8::/48"
    
    [host_network.public_internet]
    ipv6_upstream_interface = "eth0"
    resolvers = ["2606:4700:4700::1111"]
    • 示例中的地址池仅用于文档和隔离测试。部署时 IPv6 地址池和上联可以用 GUA 或 ULA,由部署方提供到 node 的回程路由和实际的公网出口。node 保留 guest 的 IPv6 源地址,上游可以自行做地址转换。
    • 启用 IPv6 时,每个 VM 分配一个 /128 地址,通过链路本地网关和精确的主机路由转发。上游必须把地址池路由到 node;只给上联接口配地址不够。
    • 只用 IPv4 时设置 ["ipv4"],只配置 ipv4_pool、ipv4_upstream_interface 和 IPv4 的 DNS 服务器。
    • 双栈部署可以只配置一个地址族的 DNS 服务器。A 与 AAAA 记录类型不决定 DNS 查询走哪个地址族。
    • node 用能力 kibotos.network.ipv4、kibotos.network.ipv6 上报启用的地址族。kibotos.service.public-internet 表示所有启用族的上联检查都通过:某个启用族检查失败时撤下这个能力,禁用的族不参与检查。
    • 有网络的 node 同时上报 kibotos.network.destination-rules。ctrl 只把策略带 allow 或 deny 列表的 Workspace 放到这样的 node 上。
    • 启用 IPv6 时,public-internet 另外放行邻居发现、路径 MTU 发现与相关错误报告所需的 ICMPv6 类型。
    • 更换地址族前,先用原配置和原版本清理全部网络附着,再用新配置启动。node 读不了的记录会让启动失败。
  • 要让 Workspace 使用 public-internet 策略,宿主本身要能转发 guest 的流量。node 在启动时检查下面两项,从不修改它们:

    • 启用的每个地址族都打开了转发:net.ipv4.ip_forward=1、net.ipv6.conf.all.forwarding=1。写进 /etc/sysctl.d/ 持久保存,不要依赖 Docker 等其他软件在运行时打开它:

      printf '%s\n' 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/60-kibotos-forward.conf >/dev/null
      sudo sysctl -p /etc/sysctl.d/60-kibotos-forward.conf
    • 根命名空间里不属于 node 的 forward 链没有 drop 策略。

    • 不满足时 node 照常启动,但日志记一条 node offers no public internet egress 并写明原因,这台 node 不接收需要公网出口的 Workspace。修好之后重启 node。

  • 要让 Workspace 的策略经调用方的 SOCKS5 Egress 转发,在公网出口可用的基础上还需要下面几项。node 启动时在一个临时网络命名空间里试装一遍转发规则与透明监听来检查它们:

    • 内核提供 nftables 的 TPROXY 与 socket 匹配(nft_tproxy、nft_socket 模块或内建)、透明 socket(IP_TRANSPARENT)以及按 fwmark 选路的策略路由。发行版默认内核都满足。
    • 系统账户 kibotos-egress 存在。node 包会创建它,不用包安装时要自己创建同名的系统账户,不要加入任何组。
    • 出口进程的 socket 目录是 socket 目录旁边加 -egress 后缀的目录,默认 /run/kibotos/node-egress。它的上级目录必须允许 kibotos-egress 进入:包安装的 /run/kibotos 是 0711,满足这一条;目录本身由 node 创建,属主是 kibotos-egress,模式 0700。
    • 任一项不满足时 node 照常启动,日志记一条 node offers no forwarding through an Egress 并写明原因,这台 node 不宣告 kibotos.network.egress-socks5,不接收带 Egress 的 Workspace。修好之后重启 node。
  • 要让 Workspace 把 DNS 查询转给调用方的 Resolver,在公网出口可用的基础上还需要:

    • kibotos-egress 账户与出口进程的 socket 目录,要求同上一条。内核的 TPROXY 与 socket 匹配只是 Egress 的要求,Resolver 不需要。
    • [host_network.public_internet] 的 resolvers 至少有一项。
    • 不满足时日志记一条 node offers no Resolver 或 node offers no forwarding through an Egress and no Resolver 并写明原因,这台 node 不宣告 kibotos.network.resolver,不接收引用 Resolver 的 Workspace。
  • guest 的 DNS 由 resolvers 决定:

    • 新启动的 Workspace 只看到一个 nameserver,即 slot 的链路 DNS 地址。第一个 resolver 的地址族决定它的地址族:IPv4 时是 slot 的 IPv4 网关地址,IPv6 时是 fe80::1%eth0。
    • 第一个 resolver 是 IPv6 地址时,Workspace 要用 v0.3.0 及之后发布的 Platform:更早的 Platform 里的 guest 拒绝带接口标识的 nameserver,Workspace 启动失败。
    • 不引用 Resolver 的 Workspace 只用第一个 resolver,后面几项不用于 Workspace 的查询。它们只在直出的 Resolver 上游写成主机名时,由出口进程依次用来解析。
    • 部署方在宿主上限制 node 自身出站时,直出的 Resolver 上游一并受限,见出口进程。
  • 上游网络不再做地址转换时,目标看到的源地址如下。对方按源地址放行时,放行上联接口的 IPv4 地址与整个 ipv6_pool:

    • IPv4 是 node 上联接口的地址,这台 node 上的 Workspace 共用它。
    • IPv6 是 guest 自己的地址,取自 ipv6_pool。Workspace 每次启动都重新分配地址,释放的地址之后会分给别的 Workspace,所以单个地址标识不了某个 Workspace。

可选设置

  • 两个后端的 guest 默认都看不到 CPU 的虚拟化扩展(Intel VMX、AMD SVM),Workspace 里不能用 KVM 运行嵌套的虚拟机。确实需要时在 [backend] 里设 nested_virtualization = true,只影响之后启动的 VM;打开后宿主内核 KVM 的嵌套虚拟化路径对每个 guest 都可达,这部分代码出过多次 guest 逃逸漏洞,宿主要及时跟进内核更新。
  • 存储池可以放在 NFS 上,这是让一个 Workspace 停止后能在另一个 node 上启动的前提;条件与代价见NFS 上的存储池。
  • 要导出 node 自己的指标时,填写 [telemetry] 段,见集群与服务指标。

数据面在反向代理后面

数据面放在反向代理后面时,两条服务连接路径对前置层的要求不同:

路径前置层要求
/v1/service 的 WebSocket(客户端默认)转发 WebSocket 升级即可,多数反向代理的默认配置就够
ServiceConnectionService/Connect 的双向流每一跳端到端 HTTP/2,且不缓冲请求体。会缓冲请求体或回源降级到 HTTP/1.1 的代理会断开它

文件传输与进程输出同样要求关闭响应缓冲。浏览器要直连数据面时,前置层必须提供公共可信证书:浏览器不信任内部 CA 签发的数据面证书。

1. 安装并加入集群

安装包,按配置一节写配置,初始化存储池,然后加入集群并启动服务:

sudo apt-get install ./kibotos-node_1.2.3_amd64.deb
sudo install -o root -g root -m 0600 \
  /usr/share/doc/kibotos-node/node.example.toml /etc/kibotos/node/config.toml
sudoedit /etc/kibotos/node/config.toml
sudo kibotos-node storage-pool init --id node-a --path /var/lib/kibotos/node
sudo kibotos-node join --config /etc/kibotos/node/config.toml --key-file node-enrollment.key
rm node-enrollment.key
sudo systemctl enable --now kibotos-node.service
sudo journalctl -u kibotos-node.service --no-pager -n 50

storage-pool init 的 --id、--path 必须与配置里 [[storage.pool]] 的 id、path 一致。这一步写入证明目录身份的标记文件,node 自己从不创建它:没有它,这个池永远不可用,也就创建不出任何 Volume 或 Workspace。

本地池的 id 用这台 node 的 ID,也就是签发注册密钥时 --node 的值;上面以 node-a 为例,每台 node 换成自己的。不能让每台 node 的本地池都叫同一个名字:ctrl 以最先登记的存储作为这个 id 的池,后面的 node 用同一个 id 声明另一个目录时,这个池会被拒绝。它是默认池时,这台 node 虽然显示 READY,却放不下任何 Workspace。只有多台 node 共用的池(例如 NFS 上的)才在各台配置里写同一个 id,见NFS 上的存储池。

2. 确认加入成功

  • 在每台宿主机上用 version --json 核对二进制版本。

  • Operator 执行 kibo node get node-a --json,确认状态为 ADMITTED、READY,且会话中的版本与预期一致。

  • kibo node get node-a 的 Clock offset 应在毫秒级,Clock sync 为 synchronized,否则见时间同步。

  • kibo node get node-a 的 Storage pools 一节里,默认池应为 available。显示 unavailable 时后面跟着原因。原因说这是另一份存储,就是本地池的 id 与别的 node 重名了。ctrl 从没往这个被拒绝的池里放过东西,按下面的步骤改名即可:

    sudo systemctl stop kibotos-node.service
    sudoedit /etc/kibotos/node/config.toml   # 把本地池的 id 改成这台 node 的 ID
    sudo rm /var/lib/kibotos/node/.kibotos-storage-pool
    sudo kibotos-node storage-pool init --id node-a --path /var/lib/kibotos/node --adopt
    sudo systemctl start kibotos-node.service

    storage-pool init 从不覆盖已有的标记,所以要先删掉旧标记;node 启动后已在池目录里建过子目录,所以要加 --adopt。

  • 历史会话记录不能证明当前磁盘上的文件和正在运行的服务是同一版本。请同时核对 systemctl show 的 MainPID/ExecStart 与服务日志。

3. 设置池容量

node 第一次声明一个池之后,Operator 在 ctrl 上设置这个池的容量。设置之前,池不接收新的 Volume 与保留的可写层。

kibo storage-pool set-capacity node-a 200G
kibo storage-pool get node-a
  • 每个池只设一次。之后加入的 node 声明同一个共享池时,沿用已经设置的容量。
  • 容量按池所在存储愿意承诺的空间设置,以后扩容、缩容用同一条命令,不需要重启 node。计算方式见池容量。

注意

  • node 启动时从 /usr/lib/kibotos/node 校验 VM runtime,然后把它安装到存储目录下由代码锁定的位置。node 不会下载 runtime。
  • runtime 文件缺失或损坏时,node 在获取独占锁和创建状态之前就会失败。请重新安装包,不要用其他版本的文件替换。

本页内容