外观
特权与非特权容器
创建容器时那个 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 是创建时定下的。
实际做法是通过备份恢复来转换:
- 备份现有容器(
vzdump)。 - 恢复时在恢复界面里选择目标类型(Unprivileged 选项)。
- 恢复成新的 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,而是直接不写这一行)。
检查结果
对你的每个容器确认:
- 它是特权还是非特权?
- 如果是特权的,为什么?理由站得住吗?
- 它有没有监听可以从外部访问的端口?特权 + 对外服务 = 需要立刻处理。
- Features 里开了哪些?每一项都是必需的吗?