TT Lab
开始
学习 学习路径 课程

Kubernetes 发行版 — 自己搭

加了一行 profile: cis,RKE2 拒绝启动

在 TT Lab 中继续学习

目标

在 VM 中的一台 RKE2 v1.36.4+rke2r1 服务器上读取默认配置,通过真实的失败消息了解改为 profile: cis 时主机必须具备什么, 并确认 Pod Security Admission 从 privileged 变为 restricted 之后同一个特权 Pod 被拒绝,以及 etcd 快照。

为什么重要

RKE2 与 k3s 同属一个系列,是单二进制发行版,但存储从一开始就是内置 etcd,并且被设计为用一个 profile 就能开启安全加固。 因此,迁移时遇到的阻碍不是功能,而是策略和主机准备。cis profile 会让 etcd 以专用用户运行,不允许 kubelet 修改内核参数, 并对所有命名空间强制执行 restricted 标准。profile 不会替你满足这些要求,只会检查—— 在没有准备好的主机上,它会直接拒绝启动。另外,如果之后才在已经以默认 profile 运行的集群上开启它,只停止服务而留下的 root etcd 会造成新的问题。 如果在了解哪些默认值会改变的前提下进行迁移,就能提前避免“昨天还能启动的 Pod,今天被拒绝了”这类事故。

步骤

  1. 在 /root/rke2/layout.json 中写入 version(rke2 --version 第一行的版本,例如:v0.0.0+rke2r0 格式)、rke2_binary(rke2 可执行文件的绝对路径)、kubectl(RKE2 一并解压出的 kubectl 的绝对路径)、kubeconfig(管理员 kubeconfig 的路径)、kubectl_on_default_path(仅凭默认 PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 能否找到 kubectl,布尔值)。
  2. 在 /root/rke2/components.json 中写入 cni(根据 kube-system 中的 CNI DaemonSet 判断出的名称,小写)、ingress_class(默认 IngressClass 的名称)、datastore(控制平面存储:etcd 或 sqlite)、storage_classes(StorageClass 名称排序后的数组)、cluster_cidr、service_cidr、cluster_dns(CoreDNS Service 的 IP)、overlaps_host(两个网段中是否有任何一个与宿主集群的 10.244.0.0/16、10.96.0.0/12 重叠,布尔值)。
  3. 创建命名空间 legacy,用 /root/rke2/priv-legacy.yaml 启动特权 Pod priv(镜像 public.ecr.aws/docker/library/busybox:1.37,sleep 86400,privileged: true),并让它处于 Ready。然后在 /root/rke2/pss-default.json 中写入 pss_file(RKE2 写出的 Pod Security Admission 配置文件的路径)、enforce(该文件中 defaults.enforce 的值)、etcd_process_user(当前运行 etcd 进程的用户名)、priv_uid(Pod priv 的 UID)。
  4. 在 /etc/rancher/rke2/config.yaml 中加入 profile: cis 并执行 systemctl restart rke2-server。如果失败,把 rke2-server 日志中的一行 level=fatal 原样保存到 /root/rke2/cis-fatal.txt,为了在完成主机准备之前不让重试循环运转,用 systemctl stop rke2-server 将其停止,并用 rke2-killall.sh 清理即使停止服务也会继续运行的静态 Pod(包括以 root 运行的 etcd)。
  5. 按文档中的主机要求,创建系统用户和组 etcd(没有主目录,shell 为 nologin),把 RKE2 一并解压出的 rke2-cis-sysctl.conf 复制为 /etc/sysctl.d/60-rke2-cis.conf 并应用(不要重启 systemd-sysctl,而是用 sysctl -p <파일>(占位符为文件))。这个集群是以默认 profile 创建的,所以在启动之前先确认 /var/lib/rancher/rke2/server/db/etcd/member/snap/db 的所有者,把 etcd_uid、etcd_gid、db_owner_before(사용자:그룹(占位符依次为用户和组))写入 /root/rke2/host-prep.json,然后不要动所有权,用 systemctl start --no-block rke2-server 启动服务。
  6. 等待 API 服务器对 /readyz 返回 ok,然后在 /root/rke2/cis-state.json 中写入 etcd_process_user、enforce(当前 rke2-pss.yaml 中 defaults.enforce 的值)、exempt_namespaces(该文件中 exemptions.namespaces 排序后的数组)、db_owner_after(当前 member/snap/db 的 사용자:그룹(占位符依次为用户和组))、netpol_namespaces(至少有一个 NetworkPolicy 的命名空间排序后的数组)、protect_kernel_defaults(kubelet 通过 --config-dir 读取的配置目录中的文件里,是否有 protectKernelDefaults: true,布尔值)。
  7. 创建命名空间 shop,把与第 3 步相同的特权 Pod 用 /root/rke2/priv-shop.yaml(只有命名空间是 shop)应用,并把完整的拒绝输出保存到 /root/rke2/denied.txt。然后用 /root/rke2/ok.yaml 在 shop 中启动满足 restricted 标准的 Pod ok(镜像相同,sleep 86400),使其处于 Ready。不要删除 legacy 中的 priv。
  8. 用 rke2 etcd-snapshot save --name before-shop 生成快照,并在 /root/rke2/snapshot.json 中写入 name(RKE2 加上的完整快照名称)、path(文件的绝对路径)、size(字节,数字)、sha256(文件哈希)。
  9. 在 /root/rke2/report.json 中写入 profile(config.yaml 中的值)、enforce_before、enforce_after、start_blocker(第 4 步的 fatal 所要求的内容:etcd-user、sysctl、selinux 三者之一)、legacy_priv_running(legacy 中的 priv 当前是否为 Running,布尔值)、snapshot_dir(保存快照的目录)、default_sa_automount_disabled(shop 命名空间的 default ServiceAccount 上是否设置了 automountServiceAccountToken: false,布尔值)。

参考

RKE2 把什么放在了哪里

在 /root/rke2/layout.json 中写入 version(rke2 --version 第一行的版本,例如:v0.0.0+rke2r0 格式)、rke2_binary(rke2 可执行文件的绝对路径)、kubectl(RKE2 一并解压出的 kubectl 的绝对路径)、kubeconfig(管理员 kubeconfig 的路径)、kubectl_on_default_path(仅凭默认 PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 能否找到 kubectl,布尔值)。

k3s 会把 kubectl 一并安装到 /usr/local/bin,而 RKE2 把 kubectl、crictl、ctr 放在数据目录下的 bin 中。请用 env -i PATH=... bash -c 'command -v kubectl' 确认仅凭默认 PATH 能否找到。

默认带来了什么

在 /root/rke2/components.json 中写入 cni(根据 kube-system 中的 CNI DaemonSet 判断出的名称,小写)、ingress_class(默认 IngressClass 的名称)、datastore(控制平面存储:etcd 或 sqlite)、storage_classes(StorageClass 名称排序后的数组)、cluster_cidr、service_cidr、cluster_dns(CoreDNS Service 的 IP)、overlaps_host(两个网段中是否有任何一个与宿主集群的 10.244.0.0/16、10.96.0.0/12 重叠,布尔值)。

静态 Pod 清单位于 /var/lib/rancher/rke2/agent/pod-manifests。请读取 kube-apiserver 的 --service-cluster-ip-range 和 kube-controller-manager 的 --cluster-cidr。网段是否重叠,可以用 Python ipaddress 的 overlaps 计算。

在默认 profile 下,特权 Pod 可以启动

创建命名空间 legacy,用 /root/rke2/priv-legacy.yaml 启动特权 Pod priv(镜像 public.ecr.aws/docker/library/busybox:1.37,sleep 86400,privileged: true),并让它处于 Ready。然后在 /root/rke2/pss-default.json 中写入 pss_file(RKE2 写出的 Pod Security Admission 配置文件的路径)、enforce(该文件中 defaults.enforce 的值)、etcd_process_user(当前运行 etcd 进程的用户名)、priv_uid(Pod priv 的 UID)。

RKE2 会把 PSA 配置生成为 /etc/rancher/rke2 下的文件,再传给 API 服务器。进程用户用 ps -eo user,comm 查看。这个 Pod 在后面更改 profile 之后也不要删除。

仅凭 profile: cis 这一行,RKE2 就拒绝启动了

在 /etc/rancher/rke2/config.yaml 中加入 profile: cis 并执行 systemctl restart rke2-server。如果失败,把 rke2-server 日志中的一行 level=fatal 原样保存到 /root/rke2/cis-fatal.txt,为了在完成主机准备之前不让重试循环运转,用 systemctl stop rke2-server 将其停止,并用 rke2-killall.sh 清理即使停止服务也会继续运行的静态 Pod(包括以 root 运行的 etcd)。

配置文件默认不存在,需要自己创建。服务是 Restart=always,所以每 5 秒会重试一次。fatal 消息会告诉你,cis profile 对主机的要求中缺了什么。systemctl stop 只会停止 rke2 进程,容器则会保留,所以旧的 etcd 会继续以 root 运行,并且可能触碰文件。清理脚本位于安装前缀的 bin 中。

准备好 etcd 用户和内核参数,然后重新启动

按文档中的主机要求,创建系统用户和组 etcd(没有主目录,shell 为 nologin),把 RKE2 一并解压出的 rke2-cis-sysctl.conf 复制为 /etc/sysctl.d/60-rke2-cis.conf 并应用(不要重启 systemd-sysctl,而是用 sysctl -p <파일>(占位符为文件))。这个集群是以默认 profile 创建的,所以在启动之前先确认 /var/lib/rancher/rke2/server/db/etcd/member/snap/db 的所有者,把 etcd_uid、etcd_gid、db_owner_before(사용자:그룹(占位符依次为用户和组))写入 /root/rke2/host-prep.json,然后不要动所有权,用 systemctl start --no-block rke2-server 启动服务。

tarball 安装的 sysctl 文件位于安装前缀的 share/rke2 下。文档警告说,在运行中的集群上重启 systemd-sysctl,可能会与 CNI 创建的内核参数冲突并产生副作用。加固指南说明,cis profile 在启动时会把 etcd 数据目录设为 etcd 所有——默认 profile 以 root 创建的文件会变成什么样,将在下一步确认。如果在第 4 步没有把静态 Pod 也清理掉,问题就会出在这里。

确认以 cis profile 重新启动的集群

等待 API 服务器对 /readyz 返回 ok,然后在 /root/rke2/cis-state.json 中写入 etcd_process_user、enforce(当前 rke2-pss.yaml 中 defaults.enforce 的值)、exempt_namespaces(该文件中 exemptions.namespaces 排序后的数组)、db_owner_after(当前 member/snap/db 的 사용자:그룹(占位符依次为用户和组))、netpol_namespaces(至少有一个 NetworkPolicy 的命名空间排序后的数组)、protect_kernel_defaults(kubelet 通过 --config-dir 读取的配置目录中的文件里,是否有 protectKernelDefaults: true,布尔值)。

刚重启之后,API 服务器会暂时拒绝连接,请反复执行 kubectl get --raw /readyz 来等待。如果始终没有恢复,请查看 etcd 容器日志(/var/log/pods/kube-system_etcd-*)。这个版本的 kubelet 不是通过命令行标志,而是通过配置文件接收 protect-kernel-defaults(实测)——请在 ps -eo args 中找到 --config-dir,然后读取那个目录。cis profile 会由 RKE2 自己写入 PSA 配置文件和默认命名空间的 NetworkPolicy,但 NetworkPolicy 会在 API 就绪之后稍晚才生成,所以如果列表看起来是空的,请稍后再读一次。

同样的清单这次被拒绝了

创建命名空间 shop,把与第 3 步相同的特权 Pod 用 /root/rke2/priv-shop.yaml(只有命名空间是 shop)应用,并把完整的拒绝输出保存到 /root/rke2/denied.txt。然后用 /root/rke2/ok.yaml 在 shop 中启动满足 restricted 标准的 Pod ok(镜像相同,sleep 86400),使其处于 Ready。不要删除 legacy 中的 priv。

拒绝消息会把违反 restricted 的字段全部列出来。这份列表就是要修改的清单(runAsNonRoot、seccompProfile、allowPrivilegeEscalation、capabilities)。PSA 只检查创建请求,所以已经在运行的 Pod 保持原样。刚创建命名空间时,default ServiceAccount 还不存在,可能会因为其他原因被拒绝,所以请确认拒绝的原因是不是 PodSecurity。

更改 profile 之后的 etcd 快照

用 rke2 etcd-snapshot save --name before-shop 生成快照,并在 /root/rke2/snapshot.json 中写入 name(RKE2 加上的完整快照名称)、path(文件的绝对路径)、size(字节,数字)、sha256(文件哈希)。

RKE2 会在传入的名称之后加上节点名称和 Unix 时刻。rke2 etcd-snapshot ls 和 kubectl get etcdsnapshotfile 都会显示位置。关于 config.yaml 中的 profile 键会输出一行警告,但与快照无关。

整理成迁移前要确认的清单

在 /root/rke2/report.json 中写入 profile(config.yaml 中的值)、enforce_before、enforce_after、start_blocker(第 4 步的 fatal 所要求的内容:etcd-user、sysctl、selinux 三者之一)、legacy_priv_running(legacy 中的 priv 当前是否为 Running,布尔值)、snapshot_dir(保存快照的目录)、default_sa_automount_disabled(shop 命名空间的 default ServiceAccount 上是否设置了 automountServiceAccountToken: false,布尔值)。

依据前面步骤的记录文件和当前集群来填写。加固指南说明,RKE2 会直接修改系统命名空间中的 default ServiceAccount,但运维人员创建的命名空间则由运维人员负责——请在 shop 中实际确认