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,以达到读写主机文件的目的。

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

配置subuid与subgid

首先,需要把我们的用户ID设置为允许root用户(也就是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

这会在/etc/subuid/etc/subgid中添加类似下方的一行。

1
root:1000:1

其中,root代表本条记录生效的用户;1000是当前主机用户(你)的UID和GID,可能略有差异;而1代表允许使用的长度,也就是允许使用从1000开始的1个UID/GID。

配置Incus容器

现在,让我们重启Incus:

1
sudo systemctl restart incus

还差最后一步!我们现在需要让容器使用指定的UID与GID。

假设现在我们希望,让容器中的root用户,实际上具备主机中UID为1000用户的权限,也就是希望将容器内UID为0的用户,映射成主机上UID为1000的用户,我们需要执行:

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
能否在容器中使用其他用户身份?

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

如果希望映射成容器中的其他用户,还需要额外创建用户,这里为了简洁,就不采用这种做法了。

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

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

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