要求¶
硬性依赖
- glibc、musl libc、uclib 或 bionic 中的一种作为您的 C 库
- Linux 内核 >= 2.6.32
lxc-attach 的额外依赖
- Linux 内核 >= 3.8
非特权容器的额外依赖
- libpam-cgfs 为您的系统配置非特权 CGroup 操作
- 最近版本的 shadow 包含 newuidmap 和 newgidmap
- Linux 内核 >= 3.12
推荐库
- libcap(允许能力下降)
- libapparmor(为容器设置不同的 apparmor 配置文件)
- libselinux(为容器设置不同的 selinux 上下文)
- libseccomp(为容器设置 seccomp 策略)
- libgnutls(用于各种校验和)
- liblua(用于 LUA 绑定)
- python3-dev(用于 python3 绑定)
安装¶
在大多数情况下,您会在您的 Linux 发行版中找到最新版本的 LXC。直接在发行版的软件包库中,或者通过一些移植通道。
对于您的第一次 LXC 体验,我们建议您使用最新的支持版本,例如 LXC 4.0 的最新错误修复版本。
如果您使用 Ubuntu,我们建议您使用 Ubuntu 18.04 LTS 作为您的容器主机。LXC 错误修复版本在发布后不久就在发行版软件包库中直接提供,这些版本提供了干净的(未修补的)上游体验。
Ubuntu 也是为数不多的(如果不是唯一的话)默认情况下包含用于安全、非特权 LXC 容器所需的所有内容的 Linux 发行版之一。
在这样的 Ubuntu 系统上,安装 LXC 就像这样
sudo apt-get install lxc
您的系统将拥有所有 LXC 命令,以及所有模板,以及如果您想对 LXC 进行脚本化的话,python3 绑定。
使用以下命令检查 Linux 内核是否具有必需的配置
lxc-checkconfig
创建特权容器¶
特权容器是由 root 创建并作为 root 运行的容器。
特权容器是开始学习和试验 LXC 的最简单方法,但它们可能不适合生产环境使用。根据主机 Linux 发行版的不同,特权容器可能受到某些能力下降、apparmor 配置文件、selinux 上下文或 seccomp 策略的保护,但最终,进程仍然作为 root 运行,因此您永远不应该向特权容器内的非信任方提供对 root 的访问权限。即使知道特权容器安全性较低,如果您仍然必须创建特权容器,或者您的用例特别需要特权容器,那么创建它们非常简单。默认情况下,LXC 将创建特权容器。
请注意,我们在此使用的终端提示符可能与您在计算机上看到的不同。我们在此使用的终端提示符强调了我们当前是否处于主机 shell 或容器 shell 以及我们使用的用户。
使用以下命令创建一个特权容器。您可以选择任何易于记忆的容器名称。LXC 的下载模板将帮助您选择来自 https://images.linuxcontainers.org/ 的可用容器镜像
root@host:~# lxc-create --name mycontainer --template download
如果您知道要使用的容器镜像,则可以指定要发送到下载模板的选项。例如,
root@host:~# lxc-create --name mycontainer --template download -- --dist alpine --release 3.19 --arch amd64
创建容器后,您可以启动它。
root@host:~# lxc-start --name mycontainer
您可以查看有关容器的状态信息。
root@host:~# lxc-info --name mycontainer Name: mycontainer State: RUNNING PID: 3250 IP: 10.0.3.224 Link: vethgmeH9z TX bytes: 1.51 KiB RX bytes: 2.15 KiB Total bytes: 3.66 KiB
您可以查看所有容器的状态信息。
root@host:~# lxc-ls --fancy NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED mycontainer RUNNING 0 - 10.0.3.224 - false
启动容器 shell。
root@host:~# lxc-attach --name mycontainer
在容器内部,我们真正体会到系统容器是什么以及它在许多方面如何类似于轻量级虚拟机。我们在容器内部所做的更改会保留下来。如果我们稍后停止容器并重新启动它,我们的更改仍然存在。
探索容器。
root@mycontainer:~# cat /etc/os-release NAME="Alpine Linux" ID=alpine VERSION_ID=3.19.0 PRETTY_NAME="Alpine Linux v3.19" HOME_URL="https://alpinelinux.cn/" BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
更新软件包索引,升级已安装的软件包,并安装您想要使用的更多软件包。
root@mycontainer:~# apk update root@mycontainer:~# apk add --upgrade apk-tools root@mycontainer:~# apk upgrade --available root@mycontainer:~# apk add vim python3
退出容器 shell。
root@mycontainer:~# exit
您可以停止容器。
root@host:~# lxc-stop --name mycontainer
如果您不再需要该容器,则可以永久销毁它。
root@host:~# lxc-destroy --name mycontainer
自动启动¶
默认情况下,容器在主机重启时不会自动启动。我们可能在容器中有一个像 Web 应用程序这样的服务,它应该始终处于运行状态。我们希望容器在主机启动时启动。
假设我们已经创建并启动了一个名为 mycontainer
的容器,如上所述。
root@host:~# lxc-ls --fancy NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED mycontainer RUNNING 0 - 10.0.3.30 - false
我们可以通过在容器配置中添加一行来重新配置容器以自动启动。
root@host:~# echo "lxc.start.auto = 1" >>/var/lib/lxc/mycontainer/config
配置容器后,我们可以重新启动主机以测试容器是否确实自动启动。
root@host:~# reboot
在主机重新启动并重新登录主机 shell 后,我们看到容器正在运行,并且 autostart 属性已设置为 1。
root@host:~# lxc-ls --fancy NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED mycontainer RUNNING 1 - 10.0.3.30 - false
它有效!
如果我们希望我们创建的几个容器具有自动启动功能,那么我们可能更愿意创建一个新的配置文件来与 lxc-create
一起使用。
root@host:~# cp /etc/lxc/default.conf /etc/lxc/autostart.conf root@host:~# echo "lxc.start.auto = 1" >>/etc/lxc/autostart.conf root@host:~# lxc-create --name containera --config /etc/lxc/autostart.conf --template download -- --dist alpine --release 3.19 --arch amd64
作为另一个选项,如果我们希望所有容器都自动启动,那么我们可以直接修改默认的 LXC 配置。
为了安全起见,请创建原始 LXC default.conf
文件的备份。
root@host:~# cp /etc/lxc/default.conf /etc/lxc/default.conf.original
现在修改默认配置。
root@host:~# echo "lxc.start.auto = 1" >>/etc/lxc/default.conf
从现在开始,我们使用默认配置文件创建的所有容器都将具有自动启动功能。例如,
root@host:~# lxc-create --name containerb --template download -- --dist alpine --release 3.19 --arch amd64
IP 地址¶
上面,lxc-info --name mycontainer
和 lxc-ls --fancy
的输出已经向我们展示了 mycontainer
在主机本地网络上有一个 IP 地址。
如果我们启动一个容器并立即检查 lxc-ls
的输出,我们会看到容器还没有 IP 地址。
root@host:~# lxc-stop --name mycontainer root@host:~# lxc-start --name mycontainer && lxc-ls --fancy NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED mycontainer RUNNING 1 - - - false
如果我们等待大约 5 秒钟然后再次检查,那么容器将具有 IP 地址。
root@host:~# lxc-ls --fancy NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED mycontainer RUNNING 1 - 10.0.3.152 - false
如果容器没有 IP 地址,我们可能需要 配置防火墙。例如,在 Ubuntu 22.04 上
root@host:~# ufw allow in on lxcbr0 root@host:~# ufw route allow in on lxcbr0 root@host:~# ufw route allow out on lxcbr0
其中值 lxcbr0
来自 /etc/default/lxc-net
中的 LXC_BRIDGE
。
如果我们在容器中要执行需要访问互联网的操作,我们需要等到容器具有 IP 地址。一种可能的方法是轮询 lxc-info
的输出,直到它包含一个 IP 地址。
root@host:~# lxc-start --name mycontainer root@host:~# while ! lxc-info -n mycontainer | grep -Eq "^IP:\s*[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\s*$"; do sleep 1; done; echo "Container connected!" Container connected!
请注意,IP 地址 10.0.3.152
与我们之前看到的 IP 地址 10.0.3.30
不同。这是因为 IP 地址是在容器加入网络时由主机动态分配给容器的。
我们可以使用以下命令查看当前的租赁列表。
root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases 1705896165 8e:e0:fc:72:79:65 10.0.3.152 mycontainer 01:8e:e0:fc:72:79:65
停止容器会删除租赁。
root@host:~# lxc-stop --name mycontainer; root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases
重新启动容器会创建一个新的租赁,可能具有不同的 IP 地址。
root@host:~# lxc-start --name mycontainer root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases 1705896699 26:61:b7:e3:53:73 10.0.3.110 mycontainer 01:26:61:b7:e3:53:73
不要在家尝试,但是强制销毁容器不会清除容器的租赁。请友善。始终在销毁容器之前停止它。
root@host:~# lxc-destroy --force --name mycontainer root@host:~# cat /var/lib/misc/dnsmasq.lxcbr0.leases 1705896699 26:61:b7:e3:53:73 10.0.3.110 mycontainer 01:26:61:b7:e3:53:73
DHCP 预留¶
我们可能需要一个可预测的容器 IP 地址。我们可以在主机上进行 DHCP 预留,以便每次容器加入本地网络时,容器都被分配相同的 IP 地址。
要启用 DHCP 预留,我们取消 /etc/default/lxc-net
中 LXC_DHCP_CONFILE
行的注释。
root@host:~# sed -i 's|^#LXC_DHCP_CONFILE=.*$|LXC_DHCP_CONFILE=/etc/lxc/dnsmasq.conf|' /etc/default/lxc-net
添加 DHCP 预留。
root@host:~# echo "dhcp-host=mycontainer,10.0.3.100" >>/etc/lxc/dnsmasq.conf
注意:IP 地址(即上面的命令中的 10.0.3.100
)必须在 LXC_DHCP_RANGE
内。要查看 LXC_DHCP_RANGE
,请打开 /etc/lxc/dnsmasq.conf
。假设 LXC_DHCP_RANGE="10.0.1.2,10.0.1.254"
。那么上面的命令应该是
root@host:~# echo "dhcp-host=mycontainer,10.0.1.100" >>/etc/lxc/dnsmasq.conf
而不是使用 10.0.3.100
的命令。此外,IP 地址不能已被使用。选择可用 IP 地址的一种方法是使用在处理上一节时动态分配的地址之一。
重新启动 lxc-net
服务以启用 DHCP 预留。
root@host:~# service lxc-net restart
重新启动容器。(如果您在中途销毁了容器,您可能需要重新创建它。)
root@host:~# lxc-stop --name mycontainer root@host:~# lxc-start --name mycontainer
等待几秒钟,然后检查容器的 IP 地址。
root@host:~# lxc-ls --fancy NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED mycontainer RUNNING 1 - 10.0.3.100 - false
太棒了!现在我们可以依赖容器始终具有相同的 IP 地址。
添加卷挂载¶
容器的文件系统活动仅限于 /var/lib/lxc/<container-name>/rootfs
。当容器被销毁时,/var/lib/lxc/<container-name>
的所有内容也会被销毁。您可能有多个容器,并且想要在它们之间共享一些文件系统空间。您可能拥有可丢弃的容器,并且希望一些文件系统空间能够超出容器的生命周期。在这种情况下,您可以创建容器外部的主机卷,然后将该卷挂载到容器内部。
假设我们已经创建并启动了一个名为 mycontainer
的容器,如上所述。
创建主机卷。
root@host:~# mkdir -p /host/path/to/volume
要在容器内部挂载卷,有两种选择。
第一种选择需要两个步骤:手动在容器内部创建挂载目标,然后配置容器挂载。
root@host:~# lxc-attach --name mycontainer -- mkdir -p /container/mount/point root@host:~# echo "lxc.mount.entry = /host/path/to/volume container/mount/point none bind 0 0" >>/var/lib/lxc/mycontainer/config
第二种选择只需要一步:在配置挂载时使用 create=dir
,以便在容器内部自动为您创建挂载目标。
root@host:~# echo "lxc.mount.entry = /host/path/to/volume container/mount/point none bind,create=dir 0 0" >>/var/lib/lxc/mycontainer/config
对于这两种选择,请注意容器挂载目标路径 container/mount/point
是相对的。它没有前导的 /
字符。
配置容器后,重新启动它以使用新配置。
root@host:~# lxc-stop --name mycontainer root@host:~# lxc-start --name mycontainer
现在我们已经创建了卷并在容器中挂载了它,我们可以测试它是否有效。
在主机上,在卷中添加一个文本文件。
root@host:~# echo "host message" >/host/path/to/volume/messages.txt
启动容器 shell。
root@host:~# lxc-attach --name mycontainer
容器可以看到文本文件及其内容。
root@mycontainer:~# cat /container/mount/point/messages.txt host message
容器可以向文本文件添加文本。
root@mycontainer:~# echo "mycontainer message" >>/container/mount/point/messages.txt
退出容器 shell。
root@mycontainer:~# exit
主机可以看到容器的消息。
root@host:~# cat /host/path/to/volume/messages.txt host message mycontainer message
使用共享 UID 和 GID 范围以 root 身份创建非特权容器¶
创建系统范围的非特权容器(即由 root 创建和启动的非特权容器)只需要几个额外的步骤来组织下级用户 ID(uid)和下级组 ID(gid)。
具体来说,您需要手动将下级 uid 和 gid 范围分配给 /etc/subuid
和 /etc/subgid
中的 root,然后使用 lxc.idmap
条目在 /etc/lxc/default.conf
中设置这些范围。
例如,如果您在主机上没有执行与下级 uid 和 gid 范围相关的任何操作,那么以下命令可能是您需要的全部命令。在执行以下操作之前,请查看 /etc/subuid
和 /etc/subgid
,以确保范围 100000:65536 在您的主机上没有被使用。如果该范围正在使用,您可以使用其他范围。
echo "root:100000:65536" >>/etc/subuid echo "root:100000:65536" >>/etc/subgid echo "lxc.idmap = u 0 100000 65536" >>/etc/lxc/default.conf echo "lxc.idmap = g 0 100000 65536" >>/etc/lxc/default.conf
就是这样!您从现在开始以 root 身份创建的任何容器都将以非特权身份运行。例如,
lxc-create --name container1 --template download lxc-create --name container2 --template download
请注意,使用/etc/lxc/default.conf
中修改后的默认配置创建的所有容器将共享相同的从属 uid 和 gid 范围。这可能不如每个容器都有自己的从属 uid 和 gid 范围安全。
如果您启动一个容器,您可以从主机端查看正在使用的 uid 范围,以及从容器端查看的 uid 范围。
lxc-start --name container1 ps aux lxc-attach --name container1 -- ps aux
以 root 身份创建具有独立 UID 和 GID 范围的无特权容器¶
通过为每个容器使用独立的从属 uid 和 gid 范围,一个容器中的安全漏洞将无法访问其他容器。
假设我们要创建两个容器,我们可以执行以下操作。(这假设/etc/lxc/default.conf
没有像上面描述的那样被修改。)
配置并创建第一个容器,使其具有自己的 uid 和 gid 范围。
echo "root:100000:65536" >>/etc/subuid echo "root:100000:65536" >>/etc/subgid cp /etc/lxc/default.conf /etc/lxc/container1.conf echo "lxc.idmap = u 0 100000 65536" >>/etc/lxc/container1.conf echo "lxc.idmap = g 0 100000 65536" >>/etc/lxc/container1.conf lxc-create --config /etc/lxc/container1.conf --name container1 --template download
配置并创建第二个容器,使其具有不同的 uid 和 gid 范围。
echo "root:200000:65536" >>/etc/subuid echo "root:200000:65536" >>/etc/subgid cp /etc/lxc/default.conf /etc/lxc/container2.conf echo "lxc.idmap = u 0 200000 65536" >>/etc/lxc/container2.conf echo "lxc.idmap = g 0 200000 65536" >>/etc/lxc/container2.conf lxc-create --config /etc/lxc/container2.conf --name container2 --template download
创建容器后,您可以选择删除配置文件/etc/lxc/container1.conf
和/etc/lxc/container2.conf
。
以用户身份创建无特权容器¶
无特权容器是最安全的容器。它们使用 uid 和 gid 的映射来为容器分配一个 uid 和 gid 范围。这意味着容器中的 uid 0(root)实际上是容器外部的 uid 100000 之类的值。因此,如果出现问题并且攻击者设法逃离容器,他们将发现自己拥有与普通用户大致相同的权限。
不幸的是,这也意味着以下常见操作不允许:
- 大多数文件系统的挂载
- 创建设备节点
- 对映射集之外的 uid/gid 进行任何操作
因此,大多数发行版模板根本无法使用这些操作。您应该使用“下载”模板,它将为您提供已知可以在此类环境中运行的发行版的预构建映像。
以下说明假设使用最新的 Ubuntu 系统或提供类似体验的其他 Linux 发行版,即最新的内核和最新的 shadow 版本,以及 libpam-cgfs 和默认的 uid/gid 分配。
首先,您需要确保您的用户在/etc/subuid
和/etc/subgid
中定义了 uid 和 gid 映射。在 Ubuntu 系统上,系统会为每个新用户默认分配 65536 个 uid 和 gid,因此您应该已经拥有一个。如果没有,您需要使用usermod
为自己分配一个。
接下来是/etc/lxc/lxc-usernet
,它用于为无特权用户设置网络设备配额。默认情况下,您的用户不允许在主机上创建任何网络设备,要更改此设置,请添加
echo "$(id -un) veth lxcbr0 10" | sudo tee -a /etc/lxc/lxc-usernet
这意味着“your-username”可以创建最多 10 个连接到 lxcbr0 网桥的 veth 设备。
完成这些操作后,最后一步是创建一个 LXC 配置文件。
- 如果不存在,请创建
~/.config/lxc
目录。 - 将
/etc/lxc/default.conf
复制到~/.config/lxc/default.conf
- 在其中追加以下两行
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536
这些值应该与/etc/subuid
和/etc/subgid
中的值匹配,上面的值是标准 Ubuntu 系统上第一个用户的预期值。
mkdir -p ~/.config/lxc cp /etc/lxc/default.conf ~/.config/lxc/default.conf MS_UID="$(grep "$(id -un)" /etc/subuid | cut -d : -f 2)" ME_UID="$(grep "$(id -un)" /etc/subuid | cut -d : -f 3)" MS_GID="$(grep "$(id -un)" /etc/subgid | cut -d : -f 2)" ME_GID="$(grep "$(id -un)" /etc/subgid | cut -d : -f 3)" echo "lxc.idmap = u 0 $MS_UID $ME_UID" >> ~/.config/lxc/default.conf echo "lxc.idmap = g 0 $MS_GID $ME_GID" >> ~/.config/lxc/default.conf
当前的 Ubuntu LTS 20.04 需要执行以下额外的步骤
export DOWNLOAD_KEYSERVER="hkp://keyserver.ubuntu.com"
现在,使用以下命令创建您的第一个容器
systemd-run --unit=my-unit --user --scope -p "Delegate=yes" -- lxc-create --name mycontainer --template download
下载模板将向您显示可供选择的各种发行版、版本和架构。一个好的示例是“ubuntu”、“focal”(20.04 LTS)和“amd64”。另一个好的示例是“alpine”、“3.19”和“amd64”。
要以无特权用户身份运行无特权容器,该用户必须分配一个空的委托 cgroup(这是由于 cgroup2 的叶节点和委托模型,而不是 liblxc)。有关更多信息,请参阅cgroups:完全支持 cgroup2。
无法简单地以用户身份从 shell 启动容器并自动委托 cgroup。因此,您需要将对任何lxc-*
命令的调用包装在systemd-run
命令中。例如,要启动容器,请使用以下命令,而不是仅使用lxc-start mycontainer
systemd-run --unit=my-unit --user --scope -p "Delegate=yes" -- lxc-start --name mycontainer
注意:如果在安装 LXC 之前,主机上没有安装 libpam-cgfs,则需要在创建第一个容器之前确保您的用户属于正确的 cgroup。您可以通过注销并重新登录,或重新启动主机来完成此操作。
然后,您可以使用以下命令确认其状态
lxc-info --name mycontainer lxc-ls --fancy
并使用以下命令在其中获取 shell
lxc-attach --name mycontainer
可以使用以下命令停止它
lxc-stop --name mycontainer
最后,可以使用以下命令删除它
lxc-destroy --name mycontainer