Linux 容器
Linux 容器 (LXC) 是一种在单个宿主机(LXC host)运行多个隔离的 Linux 系统(容器)的操作系统级虚拟化方法。它并不提供虚拟机,而是提供了一个具有独立 CPU、内存、块 I/O、网络等空间和资源控制环境的虚拟环境。这是通过 LXC 宿主机上 Linux 内核的 namespaces 和 cgroups 特性实现的。它类似于 chroot ,但是具有更好的隔离性。
LXD 可以被用做 LXC 的管理器,本页面则涉及直接使用 LXC 。
使用容器的替代方法包括 systemd-nspawn 和 Docker 。
特权容器或非特权容器[编辑 | 编辑源代码]
LXC 容器可以被设置为以特权或非特权配置运行。
一般来说,运行非特权容器比运行特权容器更安全,因为非特权容器在设计上具有更高程度的隔离性。其中的关键在于容器内的 root UID 被映射为宿主机上的非 root UID ,这使得容器内部的攻击更难以对宿主机系统施加影响。换句话说,如果攻击者设法逃离容器,它们会发现自己在宿主机上被限制或没有权限。
Arch 的 linux包 、 linux-lts包 和 linux-zen包 内核软件包现在提供了非特权容器的开箱即用支持。 类似的,对于 linux-hardened包 软件包,非特权容器仅适用于系统管理员;对于普通用户,默认情况下禁用了 namespace ,因此需要进行额外的内核配置更改。
本文包含了用户运行任何类型容器所需的信息,但是为了运行非特权容器可能需要额外的操作。
一个例子说明非特权容器[编辑 | 编辑源代码]
为了说明 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
安装[编辑 | 编辑源代码]
需要的软件[编辑 | 编辑源代码]
安装 lxc包 和 arch-install-scripts包 使宿主机系统能够运行特权 lxc 容器。
启用非特权容器支持(可选)[编辑 | 编辑源代码]
更改 /etc/lxc/default.conf
使其包含下面的配置行:
lxc.idmap = u 0 100000 65536 lxc.idmap = g 0 100000 65536
创建 /etc/subuid
和 /etc/subgid
为每个可以运行容器的用户配置容器化 UID/GID 对的映射。下面的示例仅针对 root 用户(和 systemd 系统单元):
/etc/subuid
root:100000:65536
/etc/subgid
root:100000:65536
此外,只有在提前委派一个 cgroup 时,以非 root 用户运行非特权容器才有效( cgroup2 委派模型强制执行此限制,而不是 liblxc )。使用以下 systemd 命令来委派 cgroup (根据 LXC - Getting started: Creating unprivileged containers as a user):
$ systemd-run --unit=myshell --user --scope -p "Delegate=yes" lxc-start container_name
同样的方式也适用于其他 lxc 命令。
或者,您可以通过创建一个 systemd 单元来委派非特权 cgroup (根据 Rootless Containers: Enabling CPU, CPUSET, and I/O delegation ):
/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-hardened包 &
lxd 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) )。本节所介绍的虚拟网络类型中,大部分都需要一个宿主机上的网桥设备。
这里主要有几个可供参考的配置:
- 主机网桥
- NAT 网桥
主机网桥需要宿主机上的网络设置工具来配置一个共享网桥接口。宿主机和所有 lxc 容器将在同一个网络中被指派 IP 地址(如 192.168.1.x )。当你的目标是将一些暴露在网络上的服务如 web 服务器或 VPN 服务器容器化时,这可能更加便捷。用户可以将 lxc 当作物理 LAN 上的其他 PC ,并在路由器上为其配置相应的端口转发。增加的便捷也可以认为是增加的威胁向量,同样的,如果 WAN 流量被转发给 lxc ,将其运行在不同的网络范围上将显现更小的威胁面。
NAT 网桥不需要宿主机的网络设置工具来配置网桥。 lxc包 自带的 lxc-net
将建立一个叫 lxcbr0
的 NAT 网桥。这个 NAT 网桥是一个独立的网桥,具有不与以太网设备或物理网络桥接的私有网络。它以宿主机的一个子网的形式存在。
使用主机网桥[编辑 | 编辑源代码]
见 Network bridge 。
使用 NAT 网桥[编辑 | 编辑源代码]
安装 dnsmasq包 ,它是 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"
然后我们需要修改 LXC 容器模板使我们的容器使用我们的网桥:
/etc/lxc/default.conf
lxc.net.0.type = veth lxc.net.0.link = lxcbr0 lxc.net.0.flags = up lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx
作为一个可选配置,创建一个配置文件来手动为任意一个容器指定 IP 地址:
/etc/lxc/dnsmasq.conf
dhcp-host=playtime,10.0.3.100
Tip: To enable DHCP reservations,确保取消注释:#LXC_DHCP_CONFILE=/etc/lxc/dnsmasq.conf:
root@host:~# sed -i 's|^#LXC_DHCP_CONFILE=.*$|LXC_DHCP_CONFILE=/etc/lxc/dnsmasq.conf|' /etc/default/lxc-net
现在 start 并 enable lxc-net.service
来建立网桥接口。
防火墙相关[编辑 | 编辑源代码]
基于宿主机运行的防火墙类型,可能需要允许 lxcbr0
的入口流量进入宿主机,以及 lxcbr0
的出口流量穿过宿主机进入其他网络。为了测试这是否可以实现,尝试启动一个容器并使用 DHCP 自动获取 IP 地址,检查 lxc-net
是否能够为容器注册一个 IP 地址。(如果 IP 地址并没有被成功分配,可以用 lxc-ls -f
检查,宿主机有必要更改的策略。
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" } }
另外,由于 lxc 运行在 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 流量转发到 lxc 的 22 端口上。
为了从 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 用户创建并启动容器,需要进行额外的配置。
建立 usernet 文件名为 /etc/lxc/lxc-usernet
。根据 lxc-usernet(5),每一行配置格式为:
user type bridge number
为每个需要建立容器的用户添加配置。 bridge 需要和 /etc/default/lxc-net
中定义的一样。
在非 root 用户的家目录还需要一份 /etc/lxc/default.conf
配置文件的拷贝,如 ~/.config/lxc/default.conf
(如果有必要的话请建立该目录)。
以非 root 用户运行容器需要 ~/.local/share/
具有 +x
权限。在启动容器前使用 chmod 命令应用该更改。
创建容器[编辑 | 编辑源代码]
使用 lxc-create
命令创建容器。在 lxc-3.0.0-1 版本中,上游移除了本地存储的容器模板。
使用像下面这样的调用,来建立一个 Arch 容器:
# lxc-create -n playtime -t download -- --dist archlinux --release current --arch amd64
使用像下面这样的调用并从支持的列表中选择,来建立其他发行版的容器:
# lxc-create -n playtime -t download
配置容器[编辑 | 编辑源代码]
下面的例子同时适用于特权和非特权容器。主要对于非特权容器,默认会出现例子中没有出现的额外配置,包括 lxc.idmap = u 0 100000 65536
和 lxc.idmap = g 0 100000 65536
这些在启用非特权容器支持(可选)中作为可选配置添加的值。
具有网络支持的基本配置[编辑 | 编辑源代码]
当一个进程使用 /var/lib/lxc/CONTAINER_NAME/config
中定义的容器时,系统资源将被虚拟化/隔离。在默认情况下,创建容器的进程将应用一个没有网络支持的最小化配置。下面是一个具有 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
容器中的挂载点[编辑 | 编辑源代码]
对于特权容器,用户可以选择宿主机上的目录并以 bind 方式挂载在容器中。这在容器化相同架构并希望在容器和宿主机之间分享 pacman 软件包时非常有用。另一个例子是共享文件夹。配置的格式是:
lxc.mount.entry = /var/cache/pacman/pkg var/cache/pacman/pkg none bind 0 0
图形化程序相关(可选)[编辑 | 编辑源代码]
为了在宿主机上显示容器中的程序窗口,需要定义一些挂载点,使容器化的程序可以获取宿主机上的资源。向 /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
如果 LXC 客户机依然出现 permission denied 的报错,在宿主机调用 xhost +
来允许客户机连接宿主机的显示服务器。注意这种完全放开显示适配器权限可能带来的安全风险。
另外,在上面的挂载点之前添加下面的配置。
lxc.mount.entry = tmpfs tmp tmpfs defaults
VPN 相关[编辑 | 编辑源代码]
运行容器化的 OpenVPN 或 WireGuard, 见 Linux 容器/使用 VPN 。
管理容器[编辑 | 编辑源代码]
基本使用方法[编辑 | 编辑源代码]
列出所有已下载的LXC容器:
# lxc-ls -f
可以使用Systemd的lxc@CONTAINER_NAME.service
来开启start或停止stopLXC,启动Enable lxc@CONTAINER_NAME.service
能在宿主系统启动时连带启动LXC。
也可以不使用Systemd来开启或停止LXC。启动一个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 -- SOME_COMMAND
它和lxc-console相似,用于以root权限在容器内执行一条命令。若不使用 --clear-env
选项,容器将携带宿主机的环境变量值(例如$PATH
,容器使用其他发行版的环境变量可能无法执行某些指令)。
高级使用方法[编辑 | 编辑源代码]
LXC容器克隆[编辑 | 编辑源代码]
通过快照运行多个容器可以减少管理容器权限的负担(如用户管理和系统更新等)。该策略是以一个最新版本的容器为基础,需要使用容器时,克隆一个基础容器并在这个克隆的基础容器上构建应用。容器使用overlayfs挂载,overlayfs只会存储和基本容器有差异的数据,故这种策略能尽可能减少对系统和磁盘的占用。基本容器是只读的,但是通过overlayfs,由基本容器衍生的其他容器可以进行修改。
例如,设置一个基础容器,并通过以下命令以基础容器为基础创建两个快照(snapshot)容器,称之为“snap1”和“snap2”.
# lxc-copy -n base -N snap1 -B overlayfs -s # lxc-copy -n base -N snap2 -B overlayfs -s
这些快照可以像其他容器一样被启动和关闭。用户可以通过以下命令选择性的删除某些快照和其中的数据。该删除指令不会影响基础容器。
# lxc-destroy -n snap1 -f
在pi-hole和OpenVPN管理快照的脚本及系统组件可在lxc-service-snapshots获得。
将一个特权容器转换为非特权容器[编辑 | 编辑源代码]
当一个系统被设置为使用非特权容器时(见 #Enable support to run unprivileged containers (optional)[损坏的链接:无效的章节]),nsexec-bzrAUR包含一个被称为uidmapshift
的工具将某个已经存在的特权容器转换为非特权容器从而避免重新构建新容器。
- 强烈建议在使用这个工具前备份容器!!!
- 这个工具不会更改ACL内的的UID和GID,请手动变更。
可以使用以下命令来进行转换:
# uidmapshift -b /var/lib/lxc/foo 0 100000 65536
直接运行uidmapshift
可查看使用帮助。
运行Xorg程序[编辑 | 编辑源代码]
使用lxc-attach或SSH调用目标容器,并在需要使用的程序前加上X程序的DISPLAY ID。对于大多数简单的设置,显示编号通常为0
一个简单的例子,在容器内运行Firefox并在主机屏幕上显示。
$ DISPLAY=:0 firefox
或者为避免直接连接容器,以下命令可以在宿主容器使这个过程自动化。
# lxc-attach -n playtime --clear-env -- sudo -u YOURUSER env DISPLAY=:0 firefox
故障排除[编辑 | 编辑源代码]
Root用户登录失败[编辑 | 编辑源代码]
如果在使用lxc-console登录时显示以下错误:
login: root Login incorrect
以及容器的 journal 显示:
pam_securetty(login:auth): access denied: tty 'pts/0' is not secure !
删除容器内文件系统的 /etc/securetty
[1] 和 /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 时无网络连接[编辑 | 编辑源代码]
如果你通过/etc/lxc/containername/config
将网络接口设定为veth后无法访问互联网,请检查这个虚拟网口是否获得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,见网络配置#Network management.
无法找到命令[编辑 | 编辑源代码]
这些错误可能在与宿主机不同的发行板容器内执行基础命令时出现,例如使用Archlinux上运行一个Debian容器,在容器内执行ls、cat等指令可能会有这个错误。若出现此错误,应在使用lxc-attach时添加参数--clear-env
:
# lxc-attach -n container_name --clear-env
Failed at step 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.init.static会导致lxc-execute执行失败[编辑 | 编辑源代码]
lxc-execute
指令运行失败并显示以下错误: Unable to open lxc.init.static
.见 FS#63814 查看详情。
使用 lxc-start
命令重启即可。