Incus/LXD文件系统挂载与访问

当我们使用容器时,时常希望容器能挂载访问本地文件系统。例如,Docker可以将本地路径挂载为一个Volume访问。

Incus/LXD当然也提供了这一功能,但配置起来却有时略显复杂。

只读挂载

非常简单:

1
incus config device add <container-name> <device-name> disk source=/path/on/host path=/path/in/container

读写权限

然而,以上命令挂载的目录,在容器中常常只有只读权限。

这是因为,Incus非特权容器会默认将容器进程映射到不同的非特权UID/GID运行。

为了让容器内进程也能读写目录,常常可以采用下列方法。

方法1:shift选项

在添加挂载配置时,使用shift=true选项挂载,即可将容器中UID/GID映射到主机文件系统,命令如下:

1
incus config device add <container-name> <device-name> disk source=/path/on/host path=/path/in/container shift=true

这种方法看上去是最为便捷的,简洁,不需要额外配置。然而,这种方法的局限性与麻烦隐藏在背后。

Restricted path

在Debian/Ubuntu上,当属于incus组且不属于incus-admin组的用户访问Incus时,Incus会为用户创建一个独立的Incus项目(Project),并限定用户的所有访问资源都位于这一项目下。

显然,这种设计增强了系统安全性,然而,需要注意到,这个项目是资源受限的。很不幸地,挂载主机文件系统同样在限制范围内。

此时,如果用户执行上述含有shift=true选项的挂载命令,会出现以下报错:

1
Error: Failed to start device "my-vol": The "shift" property cannot be used with a restricted source path

具体来说,在普通用户Incus项目中,Restricted选项会限制挂载的源路径。可以参考Project配置文档进一步了解。

那么,如何在坚持使用shift=true选项的前提下挂载目录呢?

最简单的方法之一是,使用root身份修改用户项目配置:

1
sudo incus project edit user-1000`

找到restricted.devices.disk.paths一行,将后面的路径删除,留下一对空白的双引号即可。

1
2
- restricted.devices.disk.paths: /home/xxx
+ restricted.devices.disk.paths: ""

安全隐忧

如果上述问题只是多了一个额外的步骤,修改完项目配置就一劳永逸了。那么接下来的问题恐怕能让安全人员睡不着觉。

shift=true时,容器向挂载目录写入的文件,其用户组信息都将与容器中用户组保持一致!!!

例如,假设在容器的挂载路径中有一test.txt文件,如果其拥有者是root:root(当然也可以在容器中以root身份设置所有权),此时回到主机文件系统,test.txt文件的拥有者将会是主机上的root:root

在这种情况下,任何一个拥有了不受挂载目录限制的,具备incus组身份的用户,都等价于拥有了主机上的root权限

又或者,拥有容器root权限的,且能以主机上任意一个用户身份执行特定可知路径可执行文件的用户,都等价于拥有了主机上的root权限

很可怕吗?是的很可怕。

威胁的来源?

大体来说,任何具备了容器内root权限的人,都可以向挂载路径中以root身份写入可执行文件,并设置setgid属性与777权限。

此时只需要再在主机上获得任一用户shell,执行挂载路径中的可执行文件并setgid系统调用,即可获得root权限。

相比之下,这一问题就显得更加棘手了。

目前比较适合的解决方案是,在容器中创建非特权用户以执行代码,像保护主机root用户一样保护容器root用户

但,恐怕大家都会同意的是,到这一步为止,一切都显得有些复杂了。

方法2:UID与GID映射

另一种方法显得较为传统,当然也是在网上最容易找到的方案,也就是将容器中的进程UID与GID,映射为主机上某一用户的UID与GID,从而赋予容器进程“扮演”该用户访问文件系统,从而达到读写主机文件的目的。

这一方法的好处是,可以非常方便地保证,容器进程对挂载文件目录的权限,与主机系统上特定角色的权限保持一致。常见的场景是:

  • 如果希望容器访问我们的~家目录,只需要赋予容器和我们账户一样的UID与GID,再将我们的~挂载到容器中,剩下的事情就是一切如常了。
  • 如果希望容器按照我们想要的权限,例如只读地访问一些文件,我们可以将容器里的用户组映射到本机某一用户组上,容器中的进程也将获得被映射到的组的文件系统权限,例如www-data等。

让我们直接参考Incus/LXD主要开发者的博客吧。

配置subuid与subgid

这里的第一步是,需要把希望容器映射到的用户ID和组ID,设置为允许Incus主进程,也就是root用户使用,从而让Incus能“扮演”我们用户账户。

如果只是想让Incus扮演我们当前使用的账户,访问我们当前账户能访问的资源,那么只需要执行以下命令:

1
2
printf "root:$(id -u):1\n" | sudo tee -a /etc/subuid
printf "root:$(id -g):1\n" | sudo tee -a /etc/subgid

现在,让我们重启Incus,让配置生效:

1
sudo systemctl restart incus

接下来是具体说明,不想看的可以略过。

实际上,这一步配置主要是向/etc/subuid/etc/subgid文件添加类似下方的一行。

1
root:1000:1

这一句配置的意思是,允许root用户/组映射UID与GID,而映射的目标UID/GID范围从1000开始,root可以使用接下来的1个UID/GID,也就是1000这一个UID/GID。具体可以按需调整。

配置Incus容器

还差最后一步!我们现在需要让容器使用刚刚指定的UID与GID,真正开始扮演指定的用户或组。

依然是省流,如果只是希望test容器中的root用户(UID/GID均为0)扮演我们当前使用的账户,请直接执行:

1
printf "uid $(id -u) 0\ngid $(id -g) 0" | incus config set test raw.idmap -

接下来是详细解释。以上命令会在容器实例的配置文件中出现类似于下面的一段:

1
2
3
4
config:
raw.idmap: |-
uid 1000 0
gid 1000 0

这段配置的意思非常直观:将本机上的UID1000,映射成容器中的UID0。同理对GID也进行这一映射。

这一段配置使得容器中的root用户,实际上具备了主机中UID为1000用户的权限,因此,容器中以root身份执行的程序,实际上是以本机UID为1000的用户身份执行的。

能否在容器中使用其他用户身份?

当然可以!在上面的配置中,每一行末尾的0即表示容器中root用户的UID与GID。

如果希望映射成容器中的其他用户,假设容器内用户的UID和GID是10086,那么只需要将刚刚的命令修改成:

1
printf "uid $(id -u) 10086\ngid $(id -g) 10086" | incus config set test raw.idmap -

就可以将容器内的10086用户映射成主机上的当前用户了。

现在,我们就可以像设置只读挂载一样,将本地路径挂载到容器中,不再需要shift=true选项

1
incus config device add <container-name> <device-name> disk source=/path/on/host path=/path/in/container

至此,我们终于能让Incus容器,在保持正确权限的同时,读写主机上挂载的文件目录。