容器安全中有一个经常被忽视但杀伤力极大的配置项,就是根文件系统的读写权限。默认情况下,Kubernetes创建的容器根文件系统是可写的,这意味着一旦容器内进程被入侵,攻击者可以随意修改二进制文件、植入后门脚本、篡改系统配置。将根文件系统设为只读,攻击者即使拿到了容器内的执行权限,也无法在根文件系统上留下任何痕迹,这大幅提升了攻击成本。本文将详细讲解Kubernetes中只读根文件系统的配置方法、底层原理以及实际落地时的常见问题。

一、只读根文件系统的底层原理
要理解readOnlyRootFilesystem的作用,需要先从容器文件系统的构成说起。容器镜像由多个只读层叠加而成,运行时容器会在最顶部添加一个可写层,所有对文件系统的修改都发生在这个可写层中。这个设计让多个容器可以共享相同的镜像层,节省磁盘空间,但同时也带来了安全隐患:可写层中的任何修改在容器重启后虽然会丢失,但在容器存活期间,恶意文件可以正常落地并执行。
当在安全上下文中设置readOnlyRootFilesystem: true时,容器运行时(如containerd、CRI-O)会以只读方式重新挂载容器的根文件系统。具体来说,运行时会调用Linux的mount系统调用,将overlay文件系统的根挂载点标记为MS_RDONLY只读标志。此后容器内任何进程尝试写入根文件系统的操作都会触发EROFS错误(Read-only file system),应用程序会收到明确的写入失败提示。
这种防护的价值在于纵深防御。即使攻击者利用应用漏洞获得了容器内的代码执行能力,比如通过反序列化漏洞执行任意命令,也无法将恶意payload持久化到磁盘。配合不可变镜像的理念,容器内的进程只能读取镜像中打包好的内容,无法篡改任何文件,这让安全审计和漏洞排查也变得简单:容器行为异常时直接销毁重建即可,因为理论上不存在被篡改的可能。
二、在Kubernetes中配置只读根文件系统
Kubernetes通过Security Context机制控制容器的安全属性,只读根文件系统的配置位于容器级别。下面是一个完整的Pod定义示例:
apiVersion: v1
kind: Pod
metadata:
name: readonly-demo
spec:
containers:
- name: app
image: nginx:1.25
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: cache
mountPath: /var/cache/nginx
- name: tmp
mountPath: /tmp
volumes:
- name: cache
emptyDir: {}
- name: tmp
emptyDir: {}
这个示例中有几个关键点需要注意。readOnlyRootFilesystem是容器级配置,写在securityContext中而不是Pod级别的配置里,如果写在Pod级别会被忽略。示例中同时挂载了两个emptyDir卷,这是实际部署中最容易踩的坑:大量应用默认会往/tmp、/var/log、/var/run等路径写入文件,根文件系统只读后这些写入全部失败,应用会直接崩溃或陷入异常循环。
除了单个Pod的配置,生产环境通常需要通过策略强制所有容器启用只读根文件系统。在较新的Kubernetes版本中可以使用Pod安全标准(Pod Security Standards)的restricted级别,该级别默认要求所有容器设置readOnlyRootFilesystem: true。通过Namespace上的标签启用:
apiVersion: v1
kind: Namespace
metadata:
name: prod
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
启用后,任何未设置只读根文件系统的新Pod都会被API Server拒绝创建,已经存在的Pod不受影响,这为存量业务的改造留出了缓冲期。如果是还在使用老版本集群,也可以通过OPA Gatekeeper或Kyverno等策略引擎实现类似的准入控制,比如用Kyverno写一条ClusterPolicy校验所有容器的securityContext.readOnlyRootFilesystem字段必须为true。
三、应用兼容性问题与可写目录规划
配置只读根文件系统最大的阻力往往不是技术本身,而是应用的兼容性。以Nginx为例,它默认需要写入/var/cache/nginx存放缓存文件、写入/var/run存放pid文件,直接启用只读后会启动失败。解决思路是梳理应用的所有写入路径,逐一用卷挂载覆盖。emptyDir适合临时文件,宿主机性能好且生命周期与Pod一致;如果日志需要持久化,则应换成hostPath或持久卷,更推荐的做法是将日志输出到标准输出,交给节点上的日志采集Agent统一处理。
规划可写目录时有一个实用技巧:写入路径尽量通过配置文件指定到少数几个固定目录,比如统一收敛到/var/app/data和/var/app/logs,这样只需要挂载两个卷就能覆盖全部写入需求。对于Java应用,JVM会在/tmp下创建hsperfdata文件用于性能统计,不挂载/tmp会导致jstat等工具失效;对于运行在Kubernetes中的Java程序,还可以通过-XX:-UsePerfData参数关闭该行为,但更简单的方案还是给/tmp挂一个emptyDir。
另一个常见问题是Secret和ConfigMap的挂载。Kubernetes默认以只读方式挂载它们,这与只读根文件系统的理念天然兼容,不需要额外处理。但要警惕某些应用启动时会把配置文件复制到可写目录再做修改的模式,这类应用需要为复制目标目录单独挂载卷。排查兼容性问题的高效方法是查看容器的崩溃日志中是否出现Read-only file system字样,根据报错路径补充对应的卷挂载即可,几个迭代下来就能收敛出完整的可写路径清单。
四、与特权容器的冲突及注意事项
只读根文件系统并非万能。需要注意它与特权容器(privileged)的关系:当容器以特权模式运行时,它拥有近乎宿主机root的全部能力,攻击者可以在容器内执行mount -o remount,rw /重新把根文件系统挂载为可读写,直接绕过只读防护。因此特权容器本身就应该被严格限制,restricted安全标准同样禁止使用特权容器,两者配合才能形成有效的防线。
还有一类容器在启动时需要执行系统级修改操作,比如安装内核模块、修改sysctl参数,这类容器确实难以做到只读根文件系统。正确的做法是把这类操作移到初始化容器或 DaemonSet 中单独隔离,让主应用容器保持最小权限。如果某些老旧应用实在无法改造,可以在安全评估后通过runAsUser搭配特定的Capabilities替代特权模式,尽可能缩小攻击面。
最后要理解的是,只读根文件系统解决的是持久化问题,不能阻止内存马这类不落地的攻击方式,也不限制对挂载卷的写入。一个完整的安全体系还需要配合镜像扫描、网络策略、最小Capabilities、非root用户运行等多层手段。在实际推行时,建议先在新开发的服务上强制启用,存量服务按重要性分批改造,通过准入策略逐步收紧,最终让整个集群的容器都运行在不可变的基础设施之上,这才是云原生安全架构的正确演进方向。
Kubernetes只读根文件系统容器安全修改时间:2026-09-10 06:04:38