非特权 Pod 如何运行用户态文件系统

2023/03/19 14:56 下午 posted in  Kubernetes

FUSE(filesystem in userspace)是指用户态的文件系统。通过 FUSE 内核模块的支持,开发者只需要根据 FUSE 提供的接口实现具体的文件操作就可以实现一个文件系统,FUSE 包含一个内核模块和一个用户空间守护进程(FUSE daemon)。内核模块加载时被注册成 Linux 虚拟文件系统的一个 FUSE 文件系统驱动,此外还注册了一个 /dev/fuse 的块设备。FUSE daemon 通过 /dev/fuse 读取请求,并将结果写入 /dev/fuse,这个 FUSE 设备就充当了 FUSE daemon 与内核通信的桥梁。

在 Kubernetes 环境中,如果需要在 Pod 中运行 FUSE daemon,通常是将其设置为特权容器。当 Pod 为特权时,自然所有的权限都会有,甚至也直接享用宿主机的设备。但非特权 Pod 想要运行用户态的文件系统有点困难,主要需要两点:

  1. 挂载权限;
  2. /dev/fuse 设备的读写权限;

本篇文章主要讲解在没有特权的情况下,如何在 Pod 中运行用户态文件系统。

挂载权限

首先,mount 属于管理级别的系统调用,需要 CAP_SYS_ADMIN 权限,参考 capability 文档

       CAP_SYS_ADMIN
              Note: this capability is overloaded; see Notes to kernel
              developers, below.

              * Perform a range of system administration operations
                including: quotactl(2), mount(2), umount(2),
                pivot_root(2), swapon(2), swapoff(2), sethostname(2),
                and setdomainname(2);

CAP_SYS_ADMIN 可以在 Pod 的 .securityContext.capabilities 中设置,如下:

    securityContext:
      capabilities:
        add:
          - SYS_ADMIN

其次,有的系统开启了 Linux 内核安全模块 AppArmor,默认的 AppArmor 配置也是没有 mount 权限的,需要额外配置 mount 权限,如下:

#include <tunables/global>

profile app flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  mount,
  umount,
  capability sys_admin, 
  ...
}

在每台节点上配置好之后,在 pod 中加入 container.apparmor.security.beta.kubernetes.io/app: localhost/app 的注解,以声明使用这份 AppArmor 配置,详细信息可以参考《如何使用 AppArmor 限制应用的权限》

FUSE 设备

一个用户态的文件系统包含一个内核模块和一个用户空间 daemon 进程。内核模块加载时被注册成 Linux 虚拟文件系统的一个 fuse 文件系统驱动。此外,还注册了一个 /dev/fuse 的块设备。该块设备作为 fuse daemon 进程与内核通信的桥梁。而对于用户空间的 fuse daemon 来说,访问 /dev/fuse 设备是至关重要的。

在 Kubernetes 环境中,如果要将宿主机的某个块设备挂载进 pod 中,可以使用 Device Plugins。而 Device Plugins 需要第三方服务自己提供,实现起来也比较简单。

对于 FUSE 设备的 Device Plugins 来说,社区也有很多实现,不过都大同小异,只需要在 Device Plugins 接口 Allocate 中将宿主机的 /dev/fuse 目录挂载进容器的 /dev/fuse 并给与 rwm 权限即可。比如:

func (m *FuseDevicePlugin) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
	devs := m.devs
	var responses pluginapi.AllocateResponse

	for _, req := range reqs.ContainerRequests {
		for _, id := range req.DevicesIDs {
			log.Printf("Allocate device: %s", id)
			if !deviceExists(devs, id) {
				return nil, fmt.Errorf("invalid allocation request: unknown device: %s", id)
			}
		}
		response := new(pluginapi.ContainerAllocateResponse)
		response.Devices = []*pluginapi.DeviceSpec{
			{
				ContainerPath: "/dev/fuse",
				HostPath:      "/dev/fuse",
				Permissions:   "rwm",
			},
		}

		responses.ContainerResponses = append(responses.ContainerResponses, response)
	}

	return &responses, nil
}

上述 Device Plugins 的完整代码实现详见zwwhdls/node-device-plugin

在 Pod 中使用只需要在 resources 中申明即可,比如:

apiVersion: v1
kind: Pod
metadata:
  name: fuse
spec:
  containers:
    - name: test
      image: centos
      command: [ "sleep",  "infinity" ]
      resources:
        limits:
          hdls.me/fuse: "1"
        requests:
          hdls.me/fuse: "1"

总结

本文主要讲解了非特权 Pod 如何运行用户态文件系统,主要需要给与挂载所需权限即 CAP_SYS_ADMIN 并将宿主机的块设备 /dev/fuse 挂载进 Pod 中,其中挂载块设备需要做一些开发工作,实现一个 Device Plugin。然后就可以愉快地在非特权 Pod 中运行 FUSE daemon 了。