强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Podman 完全指南 / 05 - Rootless 模式

第 05 章 — Rootless 模式

5.1 为什么 Rootless 很重要?

Rootless(无 root 权限)容器是安全容器化的基石。Podman 从设计之初就将 Rootless 作为默认运行模式。

安全对比

Root 模式下的风险:
┌──────────────────────────┐
│       宿主机 root        │ ← dockerd / podman 以 root 运行
│  ┌────────────────────┐  │
│  │    容器 (逃逸漏洞)   │  │ → 攻击者直接获得宿主机 root 权限
│  └────────────────────┘  │
└──────────────────────────┘

Rootless 模式下的防护:
┌──────────────────────────┐
│       普通用户 uid=1000   │ ← podman 以普通用户运行
│  ┌────────────────────┐  │
│  │  用户命名空间         │  │
│  │  ┌────────────────┐ │  │
│  │  │ 容器 (逃逸漏洞)  │ │  │ → 攻击者只获得 uid=1000
│  │  └────────────────┘ │  │
│  └────────────────────┘  │
└──────────────────────────┘
安全维度 Root 模式 Rootless 模式
攻击面 守护进程以 root 运行 每个进程独立,无守护进程
容器逃逸后果 获得宿主机 root 仅获得普通用户权限
多用户隔离 强(用户命名空间隔离)
CVE 影响范围 显著缩小
特权端口 直接绑定 需额外配置

5.2 工作原理

5.2.1 用户命名空间(User Namespace)

Rootless 的核心技术是 用户命名空间映射。容器内的 root (uid=0) 被映射为宿主机的普通用户:

┌─────────────────────────────────────┐
│        宿主机 (uid=1000)             │
│                                     │
│  /etc/subuid:                        │
│  alice:100000:65536                  │
│                                     │
│  映射关系:                           │
│  宿主机 uid=1000 ← 容器内 uid=0     │
│  宿主机 uid=100000 ← 容器内 uid=1   │
│  宿主机 uid=100001 ← 容器内 uid=2   │
│  ...                                │
│  宿主机 uid=165535 ← 容器内 uid=65535│
│                                     │
│  ┌───────────────────────────────┐  │
│  │     用户命名空间               │  │
│  │  容器视角:                     │  │
│  │    uid=0 (root)               │  │
│  │    uid=1000 (app user)        │  │
│  │    uid=65534 (nobody)         │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

5.2.2 查看 UID 映射

# 查看当前用户的 UID 映射
podman unshare cat /proc/self/uid_map
#          0       1000          1
#          1     100000      65535

# 查看 GID 映射
podman unshare cat /proc/self/gid_map
#          0       1000          1
#          1     100000      65535

# 进入用户命名空间的 Shell
podman unshare bash

# 在命名空间内查看身份
id
# uid=0(root) gid=0(root)
# 注意:这是命名空间内的虚拟 root,宿主机上仍是 uid=1000

# 退出
exit

5.2.3 配置文件

Rootless 的 UID/GID 映射由以下文件控制:

# /etc/subuid — 用户级子 UID 范围
# 格式: 用户名:起始UID:数量
alice:100000:65536
bob:200000:65536

# /etc/subgid — 用户级子 GID 范围
# 格式: 用户名:起始GID:数量
alice:100000:65536
bob:200000:65536
# 查看当前用户的映射
grep $(whoami) /etc/subuid
grep $(whoami) /etc/subgid

# 添加映射(需要 root)
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)

# 或使用 useradd 命令
sudo useradd --system --shell /usr/sbin/nologin containeruser
sudo usermod --add-subuids 200000-265535 --add-subgids 200000-265535 containeruser

5.3 存储驱动配置

Rootless 模式有专门的存储配置需求。

5.3.1 OverlayFS vs FUSE-OverlayFS

存储驱动 Root 模式 Rootless 模式 性能
overlay2 ✅ 原生支持 需 Linux 5.11+ ⭐⭐⭐
fuse-overlayfs 可用 ✅ 推荐(内核 < 5.11) ⭐⭐
vfs 可用 ✅ 后备方案
# 检查内核版本
uname -r

# Linux 5.11+:原生 OverlayFS 支持(无需 fuse-overlayfs)
# Linux < 5.11:需要 fuse-overlayfs

5.3.2 存储配置文件

编辑 ~/.config/containers/storage.conf

# ~/.config/containers/storage.conf

[storage]
# 存储目录(Rootless 默认为 ~/.local/share/containers/storage)
# home = "/home/alice/.local/share/containers/storage"
driver = "overlay"

[storage.options]
# 额外存储路径(可选)
# additionalimagestores = [
#   "/mnt/shared/images",
# ]

[storage.options.overlay]
# Linux 5.11+ 无需指定 mount_program
# Linux < 5.11 需要:
mount_program = "/usr/bin/fuse-overlayfs"
mountopt = "nodev,metacopy=on"

5.3.3 共享镜像存储

多用户共享同一台服务器时,可以配置共享镜像存储以节省磁盘空间:

# 以 root 创建共享存储目录
sudo mkdir -p /var/lib/shared-containers/storage
sudo chmod 777 /var/lib/shared-containers/storage

# 拉取共享镜像(以 root)
sudo podman --root /var/lib/shared-containers/storage pull alpine:3.20

# 用户级配置添加只读共享存储
cat >> ~/.config/containers/storage.conf << 'EOF'
[storage.options]
additionalimagestores = [
    "/var/lib/shared-containers/storage",
]
EOF

# 验证:用户可以直接使用共享镜像
podman images
# 应该可以看到 alpine:3.20(来自共享存储)

5.4 网络配置

5.4.1 Rootless 网络后端

网络后端 版本要求 说明
slirp4netns Podman 所有版本 用户空间网络栈,兼容性好,性能一般
pasta Podman 4.3+ 新一代网络栈,性能接近 root 模式
lxc-user-nic 特殊配置 LXC 网络,较少使用
# 查看当前网络后端
podman info | grep networkBackend

# 切换到 pasta(推荐,需 Podman 4.3+)
# 编辑 ~/.config/containers/containers.conf
[containers]
netns = "pasta"

# 或在运行时指定
podman run --network pasta alpine ping -c1 8.8.8.8

5.4.2 端口映射限制

# Rootless 默认不能绑定 < 1024 的端口
podman run -p 80:80 nginx  # ❌ 失败

# 解决方案一:调整 sysctl(推荐)
sudo sysctl net.ipv4.ip_unprivileged_port_start=80

# 永久生效
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-podman.conf

# 解决方案二:setcap
sudo setcap cap_net_bind_service=ep /usr/bin/podman

# 解决方案三:使用端口转发
# iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080

# 解决方案四:使用反向代理(最推荐的生产方案)
podman run -d -p 8080:80 nginx  # 绑定非特权端口
# 然后配置 nginx/haproxy/caddy 从 80 转发到 8080

5.5 cgroup 管理

5.5.1 cgroup v2(推荐)

# 检查 cgroup 版本
stat -fc %T /sys/fs/cgroup
# cgroup2fs — 表示 cgroup v2

# cgroup v2 下 Rootless 资源管理
podman run --rm --memory 512m --cpus 1.5 alpine cat /sys/fs/cgroup/memory.max

# 配置 Rootless 用户的 cgroup delegation
# 需要 systemd 245+
sudo systemctl enable --now user@$(id -u).service

# 验证 delegation
systemctl --user status

5.5.2 用户级 systemd slice 配置

# 为容器用户创建专属 systemd slice
mkdir -p ~/.config/systemd/user/

cat > ~/.config/systemd/user/container-limits.conf << 'EOF'
[Slice]
# 限制该用户所有容器的总内存
MemoryMax=4G
# 限制 CPU
CPUQuota=200%
EOF

# 重新加载
systemctl --user daemon-reload

5.6 常见限制与解决方案

限制汇总表

限制 说明 解决方案
特权端口 不能绑定 < 1024 sysctl / setcap / 反向代理
设备访问 不能访问宿主机设备 需要 root 或适当权限
内核模块 不能加载内核模块 需要 root
ping 普通用户不能 ping 设置 sysctl 或文件权限
/etc/resolv.conf 不可修改 使用 --dns 参数
网络性能 slirp4netns 有开销 使用 pasta 替代
NFS 挂载 不支持 NFS overlay 使用 bind mount
AppArmor 不支持 使用 SELinux(Podman 支持)

解决 ping 权限

# 方法一:sysctl
sudo sysctl net.ipv4.ping_group_range="0 1000"

# 永久生效
echo "net.ipv4.ping_group_range=0 1000" | sudo tee /etc/sysctl.d/99-podman-ping.conf
sudo sysctl --system

# 方法二:文件权限
sudo setcap cap_net_raw+ep /usr/bin/ping

5.7 最佳实践

✅ Do’s

# 1. 始终使用 Rootless(除非有明确理由不用)
podman run ...

# 2. 使用 crun 而非 runc(内存更少,速度更快)
# ~/.config/containers/containers.conf
[engine]
runtime = "crun"

# 3. 配置足够的 subuid/subgid 范围
# /etc/subuid
alice:100000:65536  # 65536 个 UID 通常足够

# 4. 使用 pasta 替代 slirp4netns(Podman 4.3+)

# 5. 确保 cgroup v2 已启用

# 6. 使用 --userns=keep-id 让容器内 UID 与宿主机一致
podman run -v ~/data:/data:Z --userns=keep-id myapp

❌ Don’ts

# 1. 避免使用 --privileged(等同于放弃 Rootless 的安全优势)
podman run --privileged ...  # ❌ 仅在调试时使用

# 2. 避免挂载宿主机敏感目录
podman run -v /etc:/host-etc ...  # ❌ 危险

# 3. 不要在容器内使用 host 网络
podman run --network host ...  # ❌ 失去网络隔离

# 4. 避免在生产环境使用 latest 标签
podman run nginx:latest  # ❌ 使用明确版本

5.8 本章小结

知识点 要点
核心机制 用户命名空间映射(uid_map/gid_map)
配置文件 /etc/subuid + /etc/subgid
存储驱动 Linux 5.11+ 用原生 overlay,旧版用 fuse-overlayfs
网络后端 推荐 pasta(Podman 4.3+)
端口限制 sysctl 或 setcap 绑定特权端口
最佳实践 crun 运行时、keep-id 挂载、避免 privileged

下一步


扩展阅读