Linux 容器

出自 ArchWiki
(重定向自 LXC

Linux 容器 (LXC) 是 Linux 内核容器化功能的用户空间接口,提供了一种使用 操作系统级虚拟化 的方法,它使用 命名空间cgroups 和 LXC 主机上的其他 Linux 内核 capabilities(7)lxc(7) 被认为是介于 chroot 和成熟虚拟机之间的东西。

IncusLXD 可以用作 LXC 的管理器。本页面讨论直接使用 LXC。

容器的替代方案包括 systemd-nspawnDockerPodman

特权或非特权容器

LXC 支持两种类型的容器:特权非特权

通常,特权 容器被认为是 不安全的[1]

非特权容器上,容器内的 root UID 映射到主机上的非特权 UID,这使得容器内部的黑客攻击更难导致主机系统上的后果。换句话说,如果攻击者设法逃脱容器,他们应该发现自己在主机上只有有限的或没有权限。

Arch linuxlinux-ltslinux-zen 内核软件包目前提供对非特权容器的开箱即用支持。 同样,对于 linux-hardened 软件包,非特权容器仅适用于系统管理员;由于用户命名空间默认情况下对普通用户禁用,因此需要额外的内核配置更改。

本文包含用户运行任何类型容器的信息,但为了使用非特权容器,可能需要额外的步骤

用于说明非特权容器的示例

为了说明 UID 映射的威力,请考虑下来自正在运行的非特权容器的输出。在其中,我们在 ps 的输出中看到了由容器化 root 用户拥有的容器化进程

[root@unprivileged_container /]# ps -ef | head -n 5
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 17:49 ?        00:00:00 /sbin/init
root        14     1  0 17:49 ?        00:00:00 /usr/lib/systemd/systemd-journald
dbus        25     1  0 17:49 ?        00:00:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
systemd+    26     1  0 17:49 ?        00:00:00 /usr/lib/systemd/systemd-networkd

但是,在主机上,这些容器化的 root 进程实际上显示为以映射用户(ID>99999)而不是主机的实际 root 用户身份运行

[root@host /]# lxc-info -Ssip --name sandbox
State:          RUNNING
PID:            26204
CPU use:        10.51 seconds
BlkIO use:      244.00 KiB
Memory use:     13.09 MiB
KMem use:       7.21 MiB
[root@host /]# ps -ef | grep 26204 | head -n 5
UID        PID  PPID  C STIME TTY          TIME CMD
100000   26204 26200  0 12:49 ?        00:00:00 /sbin/init
100000   26256 26204  0 12:49 ?        00:00:00 /usr/lib/systemd/systemd-journald
100081   26282 26204  0 12:49 ?        00:00:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
100000   26284 26204  0 12:49 ?        00:00:00 /usr/lib/systemd/systemd-logind

安装配置

所需软件

安装 lxcarch-install-scripts 将允许主机系统运行特权 lxc。

启用运行非特权容器的支持(可选)

修改 /etc/lxc/default.conf 以包含以下行

lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536

换句话说,映射 65536 个连续 uids 的范围,从容器端 uid 0 开始,这应该是主机视角的 uid 100000,直到并包括容器端 uid 65535,主机将知道为 uid 165535。将相同的映射应用于 gids。

创建或编辑 /etc/subuidsubuid(5)/etc/subgidsubgid(5),以包含每个能够运行容器的用户容器化 uid/gid 对的映射。下面的示例仅适用于 root 用户(和 systemd 系统单元)

/etc/subuid
root:100000:65536
/etc/subgid
root:100000:65536

本文或本节的准确性存在争议。

原因:以下说明与提供的参考[2]不符(在 Talk:Linux Containers#Unprivileged_containers_for_unprivileged_users 中讨论)

此外,仅当您预先委派 cgroup 时,以非特权用户身份运行非特权容器才有效(cgroup2 委派模型强制执行此限制,而不是 liblxc)。使用以下 systemd 命令委派 cgroup(根据 LXC - 入门:以用户身份创建非特权容器

$ systemd-run --unit=myshell --user --scope -p "Delegate=yes" lxc-start container_name

这对于其他 lxc 命令也类似。

本文或本节是与 cgroups#用户委派 合并的候选对象。

注释:并非 Linux 容器特有,避免重复。(在 Talk:Linux Containers 中讨论)

或者,通过创建 systemd 单元(根据 Rootless Containers: Enabling CPU, CPUSET, and I/O delegation)委派非特权 cgroups

/etc/systemd/system/user@.service.d/delegate.conf
[Service]
Delegate=cpu cpuset io memory pids
linux-hardened 和自定义内核上的非特权容器

希望在 linux-hardened 或其自定义内核上运行非特权容器的用户需要完成几个额外的设置步骤。

首先,需要一个支持用户命名空间的内核(具有 CONFIG_USER_NS 的内核)。所有 Arch Linux 内核都支持 CONFIG_USER_NS。但是,由于更普遍的安全问题,linux-hardened 内核仅为 root 用户启用了用户命名空间。 有两种选项可以在那里创建 非特权 容器

  • 仅以 root 身份启动非特权容器。如果 sysctl 设置 user.max_user_namespaces 的当前值为 0,则为其赋予一个正值以适应您的环境(这修复了在 lxc info --show-log container_name 中看到的 Failed to clone process in new user namespace 错误)。
  • linux-hardenedlxd 5.0.0 下,您可能需要将 /etc/subuid/etc/subgid 设置为使用 root:1000000:65536 的范围。您可能还需要以特权模式启动您的第一个容器。这修复了错误 newuidmap failed to write mapping "newuidmap: uid range [0-1000000000) -> [1000000-1001000000) not allowed"
  • 启用 sysctl 设置 kernel.unprivileged_userns_clone 以允许普通用户运行非特权容器。这可以通过以 root 身份运行 sysctl kernel.unprivileged_userns_clone=1 来为当前会话完成,并且可以使用 sysctl.d(5) 使其永久生效。

主机网络配置

LXC 支持不同的虚拟网络类型和设备(请参阅 lxc.container.conf(5) § NETWORK)。主机上的桥接设备是大多数虚拟网络类型所必需的,本节将对此进行说明。

有几种主要的设置需要考虑

  1. 主机桥接
  2. NAT 桥接

主机桥接要求主机的网络管理器管理共享桥接接口。主机和任何 lxc 将在同一网络中分配 IP 地址(例如 192.168.1.x)。如果目标是将一些网络暴露的服务(如 Web 服务器或 VPN 服务器)容器化,这可能会更简单。用户可以将 lxc 视为物理 LAN 上的另一台 PC,并在路由器中相应地转发所需的端口。增加的简单性也可以被认为是增加的威胁向量,同样,如果 WAN 流量被转发到 lxc,让它在单独的范围内运行会呈现更小的威胁面。

NAT 桥接不需要主机的网络管理器来管理桥接。lxc 附带 lxc-net,它创建了一个名为 lxcbr0 的 NAT 桥接。NAT 桥接是具有专用网络的独立桥接,该专用网络未桥接到主机的以太网设备或物理网络。它作为主机中的专用子网存在。

使用主机桥接

参见 网络桥接

使用 NAT 桥接

默认情况下,lxc-net 服务配置为创建和使用桥接接口和虚拟以太网对设备,一侧分配给容器,另一侧在主机上连接到桥接。这是使用 dnsmasq 自动完成的。

要使用此设置,首先,安装 dnsmasq,它是 lxc-net 的依赖项,然后启动启用 lxc-net.service

可以通过创建 /etc/default/lxc-net 文件或使用以下模板编辑 /etc/default/lxc 文件来覆盖某些 lxc-net 默认值

/etc/default/lxc-net
# Leave USE_LXC_BRIDGE as "true" if you want to use lxcbr0 for your
# containers.  Set to "false" if you'll use virbr0 or another existing
# bridge, or mavlan to your host's NIC.
USE_LXC_BRIDGE="true"

# If you change the LXC_BRIDGE to something other than lxcbr0, then
# you will also need to update your /etc/lxc/default.conf as well as the
# configuration (/var/lib/lxc/<container>/config) for any containers
# already created using the default config to reflect the new bridge
# name.
# If you have the dnsmasq daemon installed, you'll also have to update
# /etc/dnsmasq.d/lxc and restart the system wide dnsmasq daemon.
LXC_BRIDGE="lxcbr0"
LXC_ADDR="10.0.3.1"
LXC_NETMASK="255.255.255.0"
LXC_NETWORK="10.0.3.0/24"
LXC_DHCP_RANGE="10.0.3.2,10.0.3.254"
LXC_DHCP_MAX="253"
# Uncomment the next line if you'd like to use a conf-file for the lxcbr0
# dnsmasq.  For instance, you can use 'dhcp-host=mail1,10.0.3.100' to have
# container 'mail1' always get ip address 10.0.3.100.
#LXC_DHCP_CONFILE=/etc/lxc/dnsmasq.conf

# Uncomment the next line if you want lxcbr0's dnsmasq to resolve the .lxc
# domain.  You can then add "server=/lxc/10.0.3.1' (or your actual $LXC_ADDR)
# to your system dnsmasq configuration file (normally /etc/dnsmasq.conf,
# or /etc/NetworkManager/dnsmasq.d/lxc.conf on systems that use NetworkManager).
# Once these changes are made, restart the lxc-net and network-manager services.
# 'container1.lxc' will then resolve on your host.
#LXC_DOMAIN="lxc"
提示:确保桥接的 IP 范围不干扰本地网络。选择可用 IP 地址的一种方法是使用已动态分配给容器的地址之一。可以使用 lxc-ls -f 命令检查这一点。

可选地,创建一个配置文件来手动定义任何容器的 IP 地址

/etc/lxc/dnsmasq.conf
dhcp-host=playtime,10.0.3.100
防火墙注意事项

根据主机运行的防火墙,可能需要允许来自 lxcbr0 的入站数据包到主机,以及来自 lxcbr0 的出站数据包通过主机传输到其他网络。要测试这一点,请尝试启动一个配置为使用 DHCP 进行 IP 分配的容器,并查看 lxc-net 是否能够为容器分配 IP 地址(使用 lxc-ls -f 检查)。如果未分配 IP,则需要调整主机的策略。

ufw 的用户可以简单地运行以下两行来启用此功能

# ufw allow in on lxcbr0
# ufw route allow in on lxcbr0

或者,nftables 的用户可以修改 /etc/nftables.conf(并使用 nft -f /etc/nftables.conf 重新加载它;使用 nft -cf /etc/nftables.conf 检查配置语法是否正确)以允许容器访问互联网(将 "eth0" 替换为您的系统上具有互联网访问权限的设备;使用 ip link 列出现有设备)

/etc/nftables.conf
table inet filter {
  chain input {
    ...
    iifname "lxcbr0" accept comment "Allow lxc containers"
    
    pkttype host limit rate 5/second counter reject with icmpx type admin-prohibited
    counter
  }
  chain forward {
    ...
    iifname "lxcbr0" oifname "eth0" accept comment "Allow forwarding from lxcbr0 to eth0"
    iifname "eth0" oifname "lxcbr0" accept comment "Allow forwarding from eth0 to lxcbr0"
  }
}

此外,由于容器在 10.0.3.x 子网中运行,因此需要主动将对 ssh、httpd 等服务的外部访问转发到 lxc。原则上,主机上的防火墙需要将容器上预期端口上的传入流量转发到容器。

iptables 规则示例

此规则的目标是允许 ssh 流量到 lxc

# iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 2221 -j DNAT --to-destination 10.0.3.100:22

此规则将源自端口 2221 的 tcp 流量转发到端口 22 上 lxc 的 IP 地址。

注意:确保允许主机上的 2221/tcp 流量和 lxc 上的 22/tcp 流量。

要从 LAN 上的另一台 PC ssh 进入容器,需要 ssh 到主机的端口 2221。然后,主机将把流量转发到容器。

$ ssh -p 2221 host.lan
ufw 规则示例

如果使用 ufw,请在 /etc/ufw/before.rules 的底部附加以下内容以使其持久化

/etc/ufw/before.rules

*nat
:PREROUTING ACCEPT [0:0]
-A PREROUTING -i eth0 -p tcp --dport 2221 -j DNAT --to-destination 10.0.3.100:22
COMMIT
以非 root 用户身份运行容器

要以非 root 用户身份创建和启动容器,必须应用额外的配置。

/etc/lxc/lxc-usernet 下创建 usernet 文件。根据 lxc-usernet(5),每行的条目是

user type bridge number

使用需要创建容器的用户配置该文件。桥接将与 /etc/default/lxc-net 中定义的桥接相同。

/etc/lxc/default.conf 的副本在非 root 用户的主目录中是必需的,例如 ~/.config/lxc/default.conf(如果需要,创建目录)。

以非 root 用户身份运行容器需要在 ~/.local/share/ 上具有 +x 权限。在启动容器之前,使用 chmod 进行更改。

容器创建

容器是使用 lxc-create(1) 命令构建的。随着 lxc-3.0.0-1 的发布,上游已弃用本地存储的模板。

要创建 Arch 容器

# lxc-create --name playtime --template download -- --dist archlinux --release current --arch amd64

要通过交互方式从受支持发行版的列表中选择来创建容器

# lxc-create -n playtime -t download

要查看下载模板选项的列表

# lxc-create -t download --help
提示:Btrfs 用户可以附加 -B btrfs 来创建 Btrfs 子卷以存储容器化的 rootfs。如果借助 lxc-clone 命令克隆容器,这将非常方便。ZFS 用户可以相应地使用 -B zfs
注意:想要旧版模板的用户可以在 lxc-templatesAUR 中找到它们,或者,用户可以使用 distrobuilder 构建自己的模板。

容器配置

以下示例可以用于特权非特权容器。请注意,对于非特权容器,默认情况下会存在其他行,这些行未在示例中显示,包括 #启用运行非特权容器的支持(可选) 部分中可选定义的 lxc.idmap = u 0 100000 65536lxc.idmap = g 0 100000 65536 值。

带网络的基本配置

注意:随着 lxc-1:2.1.0-1 的发布,许多配置选项已更改。现有容器需要更新;用户被引导到 v2.1 发行说明中的这些更改表。

容器特定的配置,包括进程使用容器时要虚拟化/隔离的系统资源,在 /var/lib/lxc/CONTAINER_NAME/config 中定义。阅读 lxc.container.conf(5) 以了解配置文件的语法和可能的选项。

使用模板创建容器时生成的基本配置文件。阅读 lxc.conf(5) 以获取更多信息。

默认情况下,创建过程将进行最少设置,而没有网络支持。以下是由 lxc-net.service 提供的网络的示例配置

/var/lib/lxc/playtime/config
# Template used to create this container: /usr/share/lxc/templates/lxc-archlinux
# Parameters passed to the template:
# For additional config options, please look at lxc.container.conf(5)

# Distribution configuration
lxc.include = /usr/share/lxc/config/common.conf
lxc.arch = x86_64

# Container specific configuration
lxc.rootfs.path = dir:/var/lib/lxc/playtime/rootfs
lxc.uts.name = playtime

# Network configuration
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = ee:ec:fa:e9:56:7d

容器内的挂载

可以在容器的 rootfs 外部创建一个主机卷,然后将该卷挂载到容器内部。这可能是有利的,例如,如果容器化相同的架构,并且想要在主机和容器之间共享 pacman 软件包。另一个示例可能是共享目录。阅读 lxc.container.conf(5) § MOUNT POINTS 以获取更多信息。一般语法是

lxc.mount.entry = /var/cache/pacman/pkg var/cache/pacman/pkg none bind 0 0
注意:如果使用非特权容器,这在没有主机上的文件系统权限修改的情况下将无法工作。

Xorg 程序注意事项(可选)

为了在主机的显示器上运行程序,需要定义一些绑定挂载,以便容器化程序可以访问主机的资源。

/var/lib/lxc/playtime/config
## for xorg
lxc.mount.entry = /dev/dri dev/dri none bind,optional,create=dir
lxc.mount.entry = /dev/snd dev/snd none bind,optional,create=dir
lxc.mount.entry = /tmp/.X11-unix tmp/.X11-unix none bind,optional,create=dir,ro
lxc.mount.entry = /dev/video0 dev/video0 none bind,optional,create=file

本文或本节的准确性存在争议。

原因:设置 xhost + 非常不安全,请改用 基于 Cookie 的身份验证。(在 Talk:Linux Containers 中讨论)

如果仍然在 LXC 客户机中遇到权限被拒绝错误,请在主机中调用 xhost + 以允许客户机连接到主机的显示服务器。请注意通过这样做打开显示服务器的安全隐患。此外,在之前添加以下行以上绑定挂载行。

/var/lib/lxc/playtime/config
lxc.mount.entry = tmpfs tmp tmpfs defaults

VPN 注意事项

要运行容器化的 OpenVPNWireGuard,请参阅 Linux Containers/Using VPNs

管理容器

基本用法

列出所有已安装的 LXC 容器

# lxc-ls -f

Systemd 可用于通过 lxc@CONTAINER_NAME.service 启动停止 LXC。启用 lxc@CONTAINER_NAME.service 以使其在主机系统启动时启动。

用户也可以在没有 systemd 的情况下启动/停止 LXC。启动容器

# lxc-start -n CONTAINER_NAME

停止容器

# lxc-stop -n CONTAINER_NAME

登录到容器

# lxc-console -n CONTAINER_NAME

登录后,将容器视为任何其他 Linux 系统,设置 root 密码,创建用户,安装软件包等。

附加到容器

# lxc-attach -n CONTAINER_NAME --clear-env

这与 lxc-console 几乎相同,但它会导致在容器内部以 root 提示符启动,绕过登录。如果没有 --clear-env 标志,主机将把自己的环境变量传递到容器中(包括 $PATH,因此当容器基于另一个发行版时,某些命令将无法工作)。

高级用法

LXC 克隆

需要运行多个容器的用户可以通过使用快照来简化管理开销(用户管理、系统更新等)。策略是设置并保持更新单个基本容器,然后根据需要克隆(快照)它。此策略的强大之处在于磁盘空间和系统开销真正最小化,因为快照使用 overlayfs 挂载仅写入磁盘,仅写入数据差异。基础系统是只读的,但允许通过 overlayfs 对快照进行更改。

本文或本节需要扩充。

原因:该注释需要参考。(在 Talk:Linux Containers 中讨论)
注意:由于安全考虑,当前主线 Arch Linux 内核不支持非特权容器的 overlayfs。

例如,按照上述步骤设置一个容器。在本指南中,我们将其称为“base”。现在创建 “base” 的 2 个快照,我们将其称为 “snap1” 和 “snap2”,使用以下命令

# lxc-copy -n base -N snap1 -B overlayfs -s
# lxc-copy -n base -N snap2 -B overlayfs -s
注意:如果为 “base” lxc 定义了静态 IP,则在启动 “snap1” 和 “snap2” 之前,需要在它们的配置中手动更改该静态 IP。如果要自动化该过程,则可以使用 sed 脚本自动执行此操作,但这超出了本 Wiki 部分的范围。

快照可以像任何其他容器一样启动/停止。用户可以选择使用以下命令销毁快照以及其中的所有新数据。请注意,底层的 “base” lxc 不受影响

# lxc-destroy -n snap1 -f

用于管理 pi-holeOpenVPN 快照的 Systemd 单元和包装器脚本可在 lxc-service-snapshots 中找到,以自动化该过程。

将特权容器转换为非特权容器

一旦系统配置为使用非特权容器(参见#启用运行非特权容器的支持(可选)),nsexec-bzrAUR 包含一个名为 uidmapshift 的实用程序,该实用程序能够将现有的特权容器转换为非特权容器,以避免完全重建镜像。

警告
  • 建议在使用此实用程序之前备份现有镜像!
  • 此实用程序不会在 ACL 中移动 UID 和 GID,用户需要手动移动它们!

调用该实用程序以进行转换,如下所示

# uidmapshift -b /var/lib/lxc/foo 0 100000 65536

只需调用不带任何参数的 uidmapshift 即可获得其他选项。

运行 Xorg 程序

附加到或 SSH 进入目标容器,并在调用程序时添加主机 X 会话的 DISPLAY ID 前缀。对于大多数简单的设置,显示器始终为 0。

在主机显示器中从容器运行 Firefox 的示例

$ DISPLAY=:0 firefox

或者,为了避免直接附加到或连接到容器,可以在主机上使用以下命令来自动化该过程

# lxc-attach -n playtime --clear-env -- sudo -u YOURUSER env DISPLAY=:0 firefox

技巧与窍门

在非特权容器中 ping 命令无法工作

在非特权容器中,ping 命令可能无法工作,而无需额外的配置步骤。错误示例

% ping www.google.com
ping: socktype: SOCK_RAW
ping: socket: Operation not permitted
ping: -> missing cap_net_raw+p capability or setuid?

要在容器 foo 中修复此问题,请在主机上执行以下操作

# lxc-attach -n foo -- chmod u+s /usr/bin/ping

故障排除

Root 登录失败

如果在尝试使用 lxc-console 登录时出现以下错误

login: root
Login incorrect

并且容器的 日志 显示

pam_securetty(login:auth): access denied: tty 'pts/0' is not secure !

删除 容器 文件系统上的 /etc/securetty[3]/usr/share/factory/etc/securetty。 可选地将它们添加到 /etc/pacman.conf 中的 NoExtract 以防止它们被重新安装。 有关详细信息,请参见 FS#45903

或者,在 lxc-attach 中创建一个新用户,并使用它登录到系统,然后切换到 root。

# lxc-attach -n playtime
[root@playtime]# useradd -m -Gwheel newuser
[root@playtime]# passwd newuser
[root@playtime]# passwd root
[root@playtime]# exit
# lxc-console -n playtime
[newuser@playtime]$ su

在容器配置中使用 veth 时没有网络连接

如果您无法使用配置为 veth 并在 /etc/lxc/containername/config 中设置的网络接口访问您的 LAN 或 WAN。如果虚拟接口获得了分配的 ip 并且应该正确连接到网络。

ip addr show veth0 
inet 192.168.1.111/24

您可以禁用所有相关的静态 ip 公式,并尝试通过启动的容器操作系统设置 ip,就像您通常所做的那样。

示例 container/config

...
lxc.net.0.type = veth
lxc.net.0.name = veth0
lxc.net.0.flags = up
lxc.net.0.link = bridge
...

然后通过首选方法容器内部分配 IP,另请参见 网络配置#网络管理

错误:未知命令

当在附加的容器上键入基本命令(lscat 等)时,如果容器化的 Linux 发行版相对于主机系统不同(例如,Arch Linux 主机系统中的 Debian 容器),则可能会发生错误。附加时,使用参数 --clear-env

# lxc-attach -n container_name --clear-env

错误:在 KEYRING spawning 步骤失败...

非特权容器中的服务可能会失败,并显示以下消息

some.service: Failed to change ownership of session keyring: Permission denied
some.service: Failed to set up kernel keyring: Permission denied
some.service: Failed at step KEYRING spawning ....: Permission denied

创建一个包含以下内容的 /etc/lxc/unpriv.seccomp 文件

/etc/lxc/unpriv.seccomp
2
blacklist
[all]
keyctl errno 38

然后将以下行添加到容器配置中, lxc.idmap 之后

lxc.seccomp.profile = /etc/lxc/unpriv.seccomp

已知问题

lxc-execute 因缺少 lxc.init.static 而失败

lxc-execute 失败,并显示错误消息 Unable to open lxc.init.static。有关详细信息,请参见 FS#63814

使用 lxc-start 启动容器工作正常。

参见