跳转到内容

特权与非特权容器 ​

创建容器时那个 Unprivileged container 勾选框,是 PVE 里安全影响最大的单个选项。这篇讲清楚它做了什么,以及遇到权限问题时正确的解决方向。

特权容器不是安全边界

特权容器里的 root 就是宿主机的 root(UID 0)。PVE 用 AppArmor、seccomp 和能力集裁剪来限制它,但这些是「加固」,不是「隔离」。内核的容器逃逸漏洞被发现过,也会继续被发现。

不要把以下东西放在特权容器里:

  • 任何监听公网端口的服务
  • 你没有完整审查过的代码
  • 别人可以登录的环境

这类负载要么用非特权容器,要么用虚拟机。

UID 映射:两者唯一的本质区别 ​

特权容器:容器里的 UID 0 = 宿主机的 UID 0。直接对应,没有转换。

非特权容器:容器里的 UID 被整体平移。默认映射是容器里的 0–65535 对应宿主机的 100000–165535:

text
容器内              宿主机
root    (0)   →   100000
user    (1000) →  101000

在宿主机上看一个非特权容器的进程:

sh
ps aux | grep <容器里的进程名>

你会看到属主是 100000 这样的数字,而不是 root。

这就是隔离的来源:即使容器被完全攻破,攻击者拿到的也只是宿主机上一个没有任何特权的普通用户。

映射规则在 /etc/subuid 和 /etc/subgid 里定义。

该选哪个 ​

默认选非特权。 这是 PVE 的默认值,也是绝大多数场景的正确选择。

需要考虑特权容器的场景很少,主要是:

  • 容器里的服务需要某些非特权容器拿不到的内核能力,且确认没有替代方案。
  • 一些老的容器方案迁移过来,短期内没时间改造。

「教程让我勾特权」不是理由

网上很多 LXC 教程默认用特权容器,因为这样挂载和权限最省事。省事的代价是隔离没了。 遇到权限问题的正确方向是解决 UID 映射,不是关掉隔离。

非特权容器的典型麻烦:挂载点权限 ​

这是实践中最常撞到的问题。

现象:给容器挂载了宿主机目录,容器里看到属主是 nobody:nogroup,写入报 Permission denied。

原因:宿主机目录属主是 root(UID 0),容器里的 root 是宿主机的 UID 100000,两者对不上。

方案一:改宿主机目录的属主(最简单) ​

如果这个目录只给这一个容器用,直接把属主改成映射后的 UID:

sh
# 在宿主机执行
chown -R 100000:100000 /mnt/data/appdata

之后容器里看到的属主就是 root,读写正常。

怎么算这个数字

公式:宿主机 UID = 100000 + 容器内 UID。

容器内的 root(0)→ 宿主机 100000。 容器内的 www-data(通常是 33)→ 宿主机 100033。 容器内的普通用户(1000)→ 宿主机 101000。

先在容器里 id <用户名> 查到容器内 UID,再加 100000。

方案二:用一个共享的组 ​

如果多个容器或宿主机进程都要访问同一个目录,改属主就不合适了。

做法是在宿主机建一个组(比如 GID 保持一致),把目录设成这个组可写,并给所有相关的映射 UID 加进去。具体配置因场景而异,属于进阶内容。

方案三:自定义 UID 映射 ​

可以在容器配置里指定把某个宿主机 UID 直接映射进容器,绕开 100000 的偏移。

自定义映射要改两个地方

配置在 /etc/pve/lxc/<ctid>.conf 里写 lxc.idmap 条目,同时还要在 /etc/subuid 和 /etc/subgid 里给 root 授权使用这些 ID 段,否则容器起不来。

这是威力最大也最容易配错的方案。改之前先备份配置文件,并准备好回滚:

sh
cp /etc/pve/lxc/<ctid>.conf /root/<ctid>.conf.bak

配错的症状通常是容器启动失败,看 pct start <ctid> 的报错和 journalctl -u pve-container@<ctid>。

能不能改已有容器的类型 ​

不能直接切换。 unprivileged 是创建时定下的。

实际做法是通过备份恢复来转换:

  1. 备份现有容器(vzdump)。
  2. 恢复时在恢复界面里选择目标类型(Unprivileged 选项)。
  3. 恢复成新的 CT ID,验证没问题后再删旧的。

转换后权限全乱是正常的

从特权转非特权,容器里所有文件的属主 UID 会因为映射而错位。恢复后大概率要在容器里重新 chown 一遍。

先用测试容器练一次,不要直接拿跑着服务的容器做实验。而且:验证新容器完全正常之前,不要删除旧容器和备份。

其他相关开关 ​

创建容器时的 Features 里还有几项:

选项作用风险
Nesting允许容器内再跑容器(Docker 需要)相对温和,非特权容器里也常用
FUSE允许用户态文件系统中等
NFS / SMB mount允许容器内挂载网络共享较高,会放开相关内核能力
Create Device Nodes允许创建设备节点高,谨慎

这些开关在非特权容器里也会削弱隔离

它们的存在是为了让容器能做更多事,代价是放开原本被限制的内核接口。只开你确实需要的那一项,不要因为「说不定要用」就全勾上。

判断你现有容器的类型 ​

sh
# 看单个容器
pct config <ctid> | grep unprivileged

# 批量看
for id in $(pct list | awk 'NR>1 {print $1}'); do
  printf "%s: " "$id"
  grep -q '^unprivileged: 1' /etc/pve/lxc/$id.conf && echo "非特权" || echo "特权"
done

输出里有 unprivileged: 1 的是非特权。没有这一行的就是特权容器(配置文件里不写 unprivileged: 0,而是直接不写这一行)。

检查结果 ​

对你的每个容器确认:

  1. 它是特权还是非特权?
  2. 如果是特权的,为什么?理由站得住吗?
  3. 它有没有监听可以从外部访问的端口?特权 + 对外服务 = 需要立刻处理。
  4. Features 里开了哪些?每一项都是必需的吗?

延伸阅读 ​

Proxmox VE 非官方中文使用指南,与 Proxmox Server Solutions GmbH 无隶属关系。