要求

硬性依赖

  • 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 mycontainerlxc-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-netLXC_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

发行版 LXC 文档