Podman--read-only-tmpfs选项详解:只读容器的可写 tmpfs 挂载机制
【免费下载链接】podmanPodman: A tool for managing OCI containers and pods.项目地址: https://gitcode.com/gh_mirrors/po/podman
在 Podman 中,--read-only将容器的根文件系统挂载为只读,但进程运行时通常仍需在/tmp、/run、/dev/shm等目录写入临时数据。--read-only-tmpfs正是用来控制这些目录在只读容器中的读写状态的关键选项。本文基于仓库中该选项的官方文档(docs/source/markdown/options/read-only-tmpfs.md)展开,结合命令行参数注册、specgen 挂载生成与系统测试,完整讲解其默认行为、四种组合场景、底层实现原理、Quadlet 用法与常见实战场景,帮助读者精准设计容器文件系统的读写边界。
一、选项概述与适用命令
--read-only-tmpfs是一个布尔开关,其官方文档定义如下:
当运行
--read-only容器时,在/dev、/dev/shm、/run、/tmp和/var/tmp上挂载可读写的 tmpfs。默认值为true。
该选项由一份共用文档模板生成,同时应用于podman create、podman run两个 CLI 命令,以及 Quadlet 的podman-container.unitsystemd 单元格式(文档头部注释明确标注:This option file is used in: podman podman-container.unit.5.md.in, create, run)。在命令行为 CLI 参数,在 Quadlet 中则写作ReadOnlyTmpfs=true|false。
从源码看,该标志在 cmd/podman/common/create.go 中注册:
createFlags.BoolVar( &cf.ReadWriteTmpFS, "read-only-tmpfs", cf.ReadWriteTmpFS, "When running --read-only containers mount read-write tmpfs on /dev, /dev/shm, /run, /tmp and /var/tmp", )注意其默认值取自cf.ReadWriteTmpFS(ContainerCreateOptions的初始值),该值最终由containers.conf配置驱动。也就是说,--read-only-tmpfs的默认值可以通过容器配置文件统一调整。
与--read-only的配合关系
--read-only-tmpfs单独使用没有意义,它只在与--read-only组合时生效。仓库源码在 pkg/specgenutil/specgen.go 中明确给出了这一组合逻辑:
// Only add ReadWrite tmpfs mounts iff the container is // being run ReadOnly and ReadWriteTmpFS is not disabled, // (user specifying --read-only-tmpfs=false.) localRWTmpfs := c.ReadOnly && c.ReadWriteTmpFS s.ReadWriteTmpfs = &localRWTmpfs当--read-only=false(默认)时,无论--read-only-tmpfs取何值,ReadWriteTmpfs都恒为 false,tmpfs 的读写特性完全由容器镜像决定——即/dev、/dev/shm默认可读写,/tmp、/run、/var/tmp也是镜像中的可读写目录。
二、四种组合的行为对照表
原文档给出了一张非常直观的组合矩阵,本文完整保留并逐行解读:
--read-only | --read-only-tmpfs | /(根文件系统) | /run,/tmp,/var/tmp |
|---|---|---|---|
| true | true | r/o | r/w |
| true | false | r/o | r/o |
| false | false | r/w | r/w |
| false | true | r/w | r/w |
场景一:--read-only=true+--read-only-tmpfs=true(默认组合)
根文件系统只读,同时在/tmp、/run、/var/tmp三个目录上额外挂载可写 tmpfs。这是 Podman 的默认行为——当你只写--read-only而不显式指定--read-only-tmpfs时,实际生效的就是这个组合。容器既能保证镜像层不可篡改,又能在运行时向这些标准临时目录写入数据,适合大多数“只读根文件系统 + 运行时临时数据”的应用场景。
场景二:--read-only=true+--read-only-tmpfs=false(完全只读)
根文件系统只读,且/dev与/dev/shm被标记为只读,/tmp、/run、/var/tmp上不挂载任何 tmpfs。这些目录直接暴露底层镜像中的内容,默认同样是只读的。此时容器内部不存在任何可写目录,成为一个“完全只读”的容器。若应用需要写入数据,只能通过外部卷(--volume、--mount)或外部挂载显式提供可写位置。
场景三、四:--read-only=false
根文件系统可写,/dev、/dev/shm可读写,/tmp、/run、/var/tmp使用镜像中原本的可读写目录。此时--read-only-tmpfs无论传true还是false,行为都完全相同——该选项在非只读容器中不产生任何影响。
三、底层实现原理:从选项到挂载点
--read-only-tmpfs的行为在容器创建链路中被完整实现,涉及三层:参数到 spec 的转换、挂载点生成、OCI 运行时配置的调整。
1. 参数转换为 specgen 字段
ContainerCreateOptions.ReadWriteTmpFS经过 pkg/specgenutil/specgen.go 转换为SpecGenerator.ReadWriteTmpfs指针,该字段在 pkg/specgen/specgen.go 中定义,其 JSON 名为read_write_tmpfs,并明确注释其语义:
ReadWriteTmpfsindicates that when running with a ReadOnlyFilesystem, the tmpfs should be mounted read-write。
随后通过 pkg/specgen/generate/container_create.go 中的libpod.WithReadWriteTmpfs()选项注入容器配置,最终存储于libpod层容器配置的ReadWriteTmpfs bool字段(libpod/container_config.go)。
2. tmpfs 挂载点的生成
真正的挂载点生成位于 pkg/specgen/generate/storage.go 的addReadWriteTmpfsMounts函数:
func addReadWriteTmpfsMounts(mounts map[string]spec.Mount, volumes []*specgen.NamedVolume, runPath string) map[string]spec.Mount { readonlyTmpfs := []string{"/tmp", "/var/tmp", runPath} options := []string{"rw", "rprivate", "nosuid", "nodev", "tmpcopyup"} for _, dest := range readonlyTmpfs { if _, ok := mounts[dest]; ok { continue } for _, m := range volumes { if m.Dest == dest { continue } } mnt := spec.Mount{ Destination: dest, Type: define.TypeTmpfs, Source: define.TypeTmpfs, Options: options, } mounts[dest] = mnt } return mounts }这段实现揭示了三个值得注意的细节:
- 挂载点集合:
/tmp、/var/tmp,以及runPath(运行时确定的/run路径),与文档列出的目录一致; - 挂载选项:
rw(可读写)、rprivate(私有传播)、nosuid(禁止 setuid 位)、nodev(禁止设备文件)、tmpcopyup(将上层目录已有内容复制到 tmpfs,保证/tmp等目录中镜像预置的文件仍然可见); - 冲突规避:若目标目录已存在挂载或用户显式指定的命名卷目标恰好是这些目录,则跳过自动 tmpfs 挂载,避免与用户配置冲突。
该函数仅在s.ReadWriteTmpfs != nil && *s.ReadWriteTmpfs时被调用(pkg/specgen/generate/storage.go),与文档描述完全吻合。
3./dev与/dev/shm的只读化
当--read-only=true且--read-only-tmpfs=false时,/dev与/dev/shm需要被标记为只读。这一逻辑在 OCI spec 生成阶段完成(pkg/specgen/generate/oci_linux.go):
roFS := false if s.ReadOnlyFilesystem != nil { roFS = *s.ReadOnlyFilesystem } rwTmpfs := false if s.ReadWriteTmpfs != nil { rwTmpfs = *s.ReadWriteTmpfs } if roFS && !rwTmpfs { setDevOptsReadOnly(&g) }setDevOptsReadOnly会将/dev、/dev/shm相关挂载的选项调整为只读。而在完全只读模式下,libpod层还会在复制文件到容器(如podman cp)时对/dev/shm做特别判断——container_internal_common.go中的复制逻辑仅在dstPath == "/dev/shm" && ReadWriteTmpfs时才允许写入,否则一律拒绝(libpod/container_internal_common.go 与 libpod/container_internal_common.go),从运行时层面保证“完全只读”的语义。
4. 与用户自定义挂载、卷的优先级
从addReadWriteTmpfsMounts的实现可以看出,Podman 遵循“用户显式配置优先”的原则:只要/tmp、/var/tmp、/run中任意目录已被用户挂载(无论是否 tmpfs)或已绑定命名卷,自动挂载就会跳过该目录。因此在完全只读模式下,用户可以精确地为个别目录补上可写挂载,例如--volume mydata:/data,而不会影响其他目录的只读状态。
四、Quadlet(systemd 单元)中的用法
除 CLI 外,该选项在 Quadlet 的[Container]组中写作ReadOnlyTmpfs=true|false,对应键定义在 pkg/systemd/quadlet/quadlet.go,并作为受支持的容器组键注册(pkg/systemd/quadlet/quadlet.go),在生成 systemd 单元时被翻译为--read-only-tmpfs参数(pkg/systemd/quadlet/quadlet.go)。
仓库的 e2e 测试提供了三个直观的 Quadlet 示例:
readonly.container(默认组合,ReadOnly=yes且不写ReadOnlyTmpfs):
[Container] Image=localhost/imagename ReadOnly=yes断言生成的 podman 参数为--read-only-tmpfs --read-only,即默认值true被显式带上。
readonly-tmpfs.container(显式开启可写 tmpfs):
[Container] Image=localhost/imagename ReadOnly=yes ReadOnlyTmpfs=yesreadonly-notmpfs.container(完全只读):
[Container] Image=localhost/imagename ReadOnly=yes ReadOnlyTmpfs=no断言生成的参数为--read-only-tmpfs=false --read-only,对应上文“场景二”的完全只读语义。这三个文件分别位于 test/e2e/quadlet/readonly.container、test/e2e/quadlet/readonly-tmpfs.container 与 test/e2e/quadlet/readonly-notmpfs.container。
五、实战验证与测试佐证
仓库的系统级测试 test/system/030-run.bats 用containers.conf设置read_only=true,然后对/tmp/a、/var/tmp/b、/dev/c、/dev/shm/d、/run/e这五个文件逐一执行touch,完整验证了各组合的行为:
--read-only(默认 tmpfs=true):touch全部成功——五个目录都是可写 tmpfs;--read-only=false或--read-only=false --read-only-tmpfs=true:touch全部成功(非只读模式下选项不生效);--read-only --read-only-tmpfs=false:touch全部失败,输出touch: /tmp/a: Read-only file system等五条错误——容器完全只读。
这一测试直接印证了文档中四种组合的全部行为,读者可以在本地运行podman run --read-only --read-only-tmpfs=false <image> touch /tmp/a复现“完全只读”的效果。
快速验证命令
# 默认组合:根文件系统只读,/tmp、/run、/var/tmp 可写 podman run --rm --read-only registry.fedoraproject.org/fedora-minimal touch /tmp/hello /var/tmp/world # 完全只读:所有目录只读,写入被拒绝 podman run --rm --read-only --read-only-tmpfs=false registry.fedoraproject.org/fedora-minimal touch /tmp/hello # 输出: touch: /tmp/hello: Read-only file system # 显式关闭只读:一切照旧 podman run --rm --read-only=false --read-only-tmpfs=true registry.fedoraproject.org/fedora-minimal touch /tmp/hello六、实践建议与注意事项
- 安全加固场景优先使用完全只读:对于不需要写临时文件的安全敏感容器(如只读代理、校验器),推荐
--read-only --read-only-tmpfs=false,再按需通过--volume或--mount挂载唯一的数据写入点,攻击面最小。 - 默认组合适合常规只读容器:很多应用(如 JVM、Python 解释器)依赖
/tmp写缓存,直接使用默认的--read-only即可获得“镜像只读 + 临时目录可写”的平衡。 - 注意
/dev与/dev/shm的差异:在默认组合下,/dev与/dev/shm上挂载的是可写 tmpfs;只有完全只读模式下它们才被标记为只读。若容器依赖共享内存(如--ipc=host下的 IPC),需留意/dev/shm的实际大小与读写状态。 - 用户挂载优先级最高:自动 tmpfs 挂载会主动避让用户显式指定的卷与挂载,这一设计保证了
--read-only与自定义挂载组合时的可预测性。 - Quadlet 中显式声明更稳妥:虽然
ReadOnlyTmpfs默认是true,但建议在.container文件中显式写出ReadOnlyTmpfs=yes|no,避免升级或配置变化带来的隐性行为差异。
通过本文的解读可以看到,--read-only-tmpfs虽只是一个布尔选项,但其背后是 Podman 从 CLI 解析、specgen 转换、挂载生成到 OCI 运行时配置的一条完整实现链路。理解这四种组合的语义,是设计安全、可预期、可运维的只读容器的关键一步。
【免费下载链接】podmanPodman: A tool for managing OCI containers and pods.项目地址: https://gitcode.com/gh_mirrors/po/podman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考