- 一、背景
- 二、文件/目录创建掩码
- 三、宿主机目录权限
- 四、容器挂载目录权限
- 五、虚拟文件系统 – 挂载机制
- 六、目录挂载权限管理
- 七、容器启动 – 进程账号
- 八、”Permission denied” 错误排查
- 九、案例分析:Redis Dockerfile
- 十、总结
一、背景
在容器化环境中,文件权限和用户管理常常会引发各种问题,例如:遇到 “Permission denied” 错误,却不知从何处着手解决。问题背后往往涉及容器文件系统、用户权限和挂载机制等细节。本文将从几个关键问题出发,系统地探讨容器中的文件、账号与权限管理,以更好地理解和解决这些问题。
- 进程创建的目录、文件,默认权限是怎么指定的?
- 宿主机不存在的路径,是由谁来创建,权限是怎样的?
- 容器的挂载路径中不存在的目录是由谁来创建的,权限是怎样的?
- 虚拟文件系统是怎么实现挂载、挂载目录读写?
- 同一个文件系统 (目录) 挂载到不同的容器 (机器),权限是怎么管理的?
- root 启动的容器,进程启动用户一定是 root 么?
- “Permission denied” 错误如何排查?
本文从最基础的 Linux 文件权限系统开始,逐步深入分析容器中的权限机制。
二、文件/目录创建掩码
在 Linux 系统中,当进程创建新文件或目录时,它们的默认权限是由进程的 umask(用户文件创建模式掩码)决定的。这个机制在容器环境中同样适用,是理解权限问题的关键。
2.1 umask 的作用
umask 是一个三位或四位八进制数,每一位分别对应文件权限的用户(owner)、用户组(group)和其他人(others)。它定义了从基准权限中需要减去哪些权限位。
2.2 权限计算机制
当创建新文件或目录时:
- 文件的基准权限是 0666(
rw-rw-rw-
) - 目录的基准权限是 0777(
rwxrwxrwx
) - 实际权限 = 基准权限 - umask
例如,如果 umask 是 022:
- 新文件权限:0666 - 022 = 0644(
rw-r--r--
) - 新目录权限:0777 - 022 = 0755(
rwxr-xr-x
)
容器中可通过以下命令查看和设置 umask:
# 查看当前 umask
umask
0022
# 设置新的 umask
umask 0027
设置 umask 为 0027 后,新创建的文件默认权限为 0640(rw-r-----
),新创建的目录默认权限为 0750(rwxr-x---
)。
以下是一个简单的验证实验:
# 设置 umask 为 0022
umask 0022
# 创建文件和目录
touch test-file
mkdir test-dir
# 检查权限
ls -l test-file
-rw-r--r-- 1 root root 0 Jun 14 10:15 test-file
ls -ld test-dir
drwxr-xr-x 2 root root 4096 Jun 14 10:15 test-dir
# 修改 umask 为 0027
umask 0027
# 创建文件和目录
touch test-file-2
mkdir test-dir-2
# 检查权限
ls -l test-file-2
-rw-r----- 1 root root 0 Jun 14 10:16 test-file-2
ls -ld test-dir-2
drwxr-x--- 2 root root 4096 Jun 14 10:16 test-dir-2
理解 umask 机制是掌握容器环境中目录和文件权限设置的基础。下面将分析这一机制在宿主机和容器环境中的具体应用。
三、宿主机目录权限
在 Kubernetes 中,当需要将主机上的目录挂载到容器内时,常常会使用 HostPath 卷。如果该主机目录不存在,可以使用 HostPathType 设置为 DirectoryOrCreate 来自动创建该目录。这时,一个关键问题出现了:这个目录是由谁创建的?它的权限是什么?
目录由节点上的 kubelet 组件创建。kubelet 是运行在每个节点上的 Kubernetes 代理,负责管理 Pod 和容器生命周期。如果目标目录不存在,kubelet 会在挂载卷时自动创建该目录。
目录的权限:
- 所有者:目录的所有者为 kubelet 进程的运行用户(通常为
root
,但取决于节点上的 kubelet 配置)。 - 权限:默认权限为 0755(即
drwxr-xr-x
),遵循 Linux 的默认目录权限规则。如果 kubelet 的配置被修改过,权限可能会变化,但通常情况下默认是0755
。
以下是一个实际的例子:
apiVersion: v1
kind: Pod
metadata:
name: hostpath-pod
spec:
containers:
- name: mycontainer
image: busybox:1.28
command: ["/bin/sh"]
args: ["-c", "sleep 3600"] # 使容器保持运行
volumeMounts:
- mountPath: /container/path
name: host-volume
volumes:
- name: host-volume
hostPath:
path: /data/log/hostpath-pod
type: DirectoryOrCreate
当该 Pod 被创建后,可以检查宿主机上自动创建的目录权限:
# 查看 Pod 所在宿主机
kubectl get pod hostpath-pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
hostpath-pod 1/1 Running 0 16m 192.168.1.100 worker-node-01 <none> <none>
# 登录宿主机查看目录权限
ls -ld /data/log/hostpath-pod/
drwxr-xr-x 2 root root 4096 Jun 13 10:06 /data/log/hostpath-pod/
如上所示,自动创建的目录属于 root 用户和 root 组,权限为 0755,这与 umask 0022 结合基础权限 0777 的结果一致。
四、容器挂载目录权限
类似地,在容器挂载过程中,如果容器内的挂载路径不存在,Kubelet 也会自动创建这个路径。这里的权限管理与宿主机上相似,同样受 umask 影响:
容器内路径的创建
- 自动创建逻辑:
无论挂载的是 HostPath 卷、emptyDir 卷还是其他类型的卷,如果容器内的挂载路径(如/app/data
)不存在,Kubelet 会在容器启动前自动创建该路径。 - 创建者身份:
容器内的路径由 Kubelet 以容器运行时(如 Docker、containerd)的默认用户身份创建。默认情况下,容器运行时可能以root
用户运行(除非显式配置了非 root 用户)。
继续使用上面的例子,进入容器检查挂载目录的权限:
# 进入容器
kubectl exec -it hostpath-pod -- /bin/sh
# 检查挂载目录权限
ls -ld /container/path
drwxr-xr-x 2 root root 4096 Jun 13 02:06 /container/path
在容器内,挂载目录同样属于 root 用户和 root 组,权限为 0755,这再次验证了 umask 机制在容器环境中的应用。
理解了基本的权限机制后,需要进一步分析更复杂的场景:当同一个文件系统被挂载到不同的容器时,权限是如何管理的?这涉及到虚拟文件系统的实现。
五、虚拟文件系统 – 挂载机制
要深入理解容器中的文件权限管理,必须了解底层的虚拟文件系统挂载原理。容器环境中的挂载实际依赖于 Linux VFS(虚拟文件系统) 层的实现。
- 目录游走:目录游走是逐渐实例化该组件对应的 inode 和 dentry 的过程。在没有任何缓存的情况下,dentry 会先被初始化,在 dentry 中包含文件/目录名字符串。在具体某一级目录中,会调用该目录 inode 的 lookup() 函数查找该目录中的对应子项(子目录或子文件),然后完成子项 dentry 和 inode 的初始化
挂载点初始化:涉及挂载的关键信息的初始化在挂载的时候就已经完成。即,为源目录添加挂载点标记,同时添加挂载信息(包括,源和目标文件系统的信息)到挂载点列表
挂载点游走:在目录游走时,如果发现该目录标记为挂载点,则从挂载点列表寻找目标文件系统的信息,然后从目标文件系统继续往下遍历
这种挂载机制是理解不同类型的卷在权限处理上存在差异的基础。当目录被挂载后,访问该目录的进程实际上会穿过挂载点,访问到目标文件系统上的内容,而权限检查则会基于目标文件系统上的权限设置。
有了这些挂载机制的基础知识,接下来可以进一步分析 Kubernetes 中如何管理挂载卷的权限。
六、目录挂载权限管理
基于前面介绍的挂载机制,下面分析容器环境中如何管理挂载卷的权限。当文件系统挂载到容器中时,权限管理涉及几个核心机制:
6.1 基本权限原理
文件系统的权限体系在容器环境中仍然遵循 Linux 标准:
- 权限继承:挂载的文件和目录保留其原始的 UID/GID 和权限位
- 用户映射:容器内的进程根据其 UID/GID 访问文件,若容器内不存在对应用户,则直接显示数字 ID
# 宿主机上以特定用户创建文件
cyningsun$> touch uid-gid.txt
# 查看文件归属用户、用户组
$> ls -l /data/cyningsun/k8s/dir/uid-gid.txt
-rw-r--r-- 1 cyningsun dev 0 6月 13 11:29 /data/cyningsun/k8s/dir/uid-gid.txt
# 查看用户 UID、GID
$> id cyningsun
uid=1002(cyningsun) gid=1001(dev) groups=1001(dev),1002(dev_sudo)
# 查看文件归属 UID、GID
$> ls -ln /data/cyningsun/k8s/dir/uid-gid.txt
-rw-r--r-- 1 1002 1001 0 6月 13 11:29 /data/cyningsun/k8s/dir/uid-gid.txt
6.2 Pod 安全上下文
Kubernetes 通过 SecurityContext 控制 Pod 和容器的权限:
apiVersion: v1
kind: Pod
metadata:
name: security-context-pod
spec:
securityContext:
runAsUser: 1000 # 进程用户ID
runAsGroup: 2000 # 进程组ID
fsGroup: 2000 # 文件系统组ID
containers:
- name: busybox-container
image: busybox:1.28
command: ["/bin/sh"]
args: ["-c", "sleep 3600"] # 使容器保持运行
volumeMounts:
- mountPath: /container/path
name: host-volume
volumes:
- name: host-volume
hostPath:
path: /data/log/security-context-pod
关键参数作用:
runAsUser/runAsGroup
: 控制容器进程的用户/组身份fsGroup
: 控制挂载卷的组权限,影响现有文件和新创建文件的组所有权
6.3 不同卷类型的权限特性
Kubernetes 中不同卷类型对 fsGroup
的处理机制存在显著差异,这主要与它们的存储实现原理、安全模型和权限管理方式有关:
持久卷(PersistentVolume,如 AWS EBS、Azure Disk、NFS 等):
- 支持
fsGroup
: 对大多数类型的 PV,fsGroup
设置生效 - 原因:
- 存储驱动支持:持久卷通常由云服务商或分布式存储系统提供,其存储驱动支持 Kubernetes 的所有权和权限动态修改
- 多租户隔离需求:PV 是集群级别的资源,设计上需要支持多租户场景,确保不同用户或 Pod 挂载同一卷时能通过
fsGroup
自动隔离权限
- 生效机制:
- 挂载时自动递归修改卷内所有文件和目录的组所有权为
fsGroup
值 - 确保组权限生效,例如设置目录的 setgid 位,使新创建的文件继承父目录的组所有权
- 若同一 PV 被多个 Pod 挂载,后挂载的 Pod 的
fsGroup
会覆盖之前的设置
- 挂载时自动递归修改卷内所有文件和目录的组所有权为
- 持久性:
fsGroup
的修改是持久的,即使 Pod 删除后也会保留
- 支持
临时卷(emptyDir):
- 支持
fsGroup
:fsGroup
对 emptyDir 卷生效 - 原因:
- emptyDir 卷是 Pod 级别的临时存储,完全受 Kubernetes 控制
- 每次 Pod 创建时都会新建,无需考虑多租户权限冲突
- 生效机制:
- 卷创建时自动应用
fsGroup
设置 - 确保 Pod 中的所有容器都能通过组权限访问卷内文件
- 卷创建时自动应用
- 临时性:Pod 删除时卷内容被清除,权限问题不会持续存在
- 支持
hostPath 卷:
- 不支持
fsGroup
:fsGroup
设置对 hostPath 卷不生效 - 原因:
- 直接绑定宿主机文件系统:hostPath 卷直接挂载宿主机上的目录或文件,权限完全依赖宿主机现有设置
- 安全限制:Kubernetes 设计上避免自动修改宿主机文件系统权限,防止因权限篡改引发安全风险
- 存储驱动不支持:hostPath 卷的实现不包含动态修改文件组所有权的逻辑
- 权限行为:
- 挂载后,容器内访问的目录权限与宿主机目录完全一致,不会触发任何所有权或权限修改
- 如需修改宿主机上的文件权限,需要通过初始化容器或其他机制手动设置
- 权限协调:若容器进程用户需要访问 hostPath 目录,必须手动设置宿主机目录的权限
- 不支持
6.4 新文件创建的权限规则
当 Pod 内进程创建新文件时,权限规则根据卷类型有所不同:
6.4.1 emptyDir 和持久卷 (PV) 的新文件
当进程在 emptyDir 卷或支持 fsGroup 的持久卷上创建新文件时:
- 文件所有者:为创建进程的 UID(受
runAsUser
影响) - 文件组:为 Pod 的
fsGroup
值,不受进程主组影响 - 权限位:由基准权限减去 umask 值计算得出
- 文件基准权限:0666 (
rw-rw-rw-
) - 目录基准权限:0777 (
rwxrwxrwx
) - 常见 umask 为 0022,则新文件权限为 0644 (
rw-r--r--
)
- 文件基准权限:0666 (
6.4.2 hostPath 卷的新文件
当进程在 hostPath 卷上创建新文件时:
- 文件所有者:为创建进程的 UID(受
runAsUser
影响) - 文件组:为创建进程的主组 ID(受
runAsGroup
影响),不会应用 Pod 的fsGroup
值 - 权限位:同样受 umask 影响,但权限最终保存在宿主机文件系统上
这种差异解释了为什么在使用 hostPath 卷时,即使设置了 fsGroup
,新创建的文件组所有权也不会按预期设置。
通过这种机制,Kubernetes 在支持 fsGroup 的卷类型上确保了跨容器的文件权限一致性,同时对 hostPath 卷保持了安全限制。
七、容器启动 – 进程账号
在容器环境中,一个重要问题是:当以 root 用户启动容器时,容器内的进程是否也一定以 root 用户运行?答案并不总是肯定的。
在很多容器镜像中,即使以 root 用户启动容器,实际运行的应用进程可能会自动降级到非 root 用户。这是一种安全最佳实践,因为以 root 用户运行容器会有以下风险:
- 进程拥有容器内的全部权限
- 如果有数据卷映射到宿主机,容器内的 root 用户可能会影响宿主机文件
这种权限降级通常通过 docker-entrypoint.sh 脚本实现。以下是一个简化的示例:
#!/bin/sh
set -e
# 如果是以 root 用户运行 redis-server 命令
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
# 将当前目录下所有非 redis 用户拥有的文件改为 redis 用户所有
find . \! -user redis -exec chown redis '{}' +
# 使用 gosu 切换到 redis 用户运行命令
exec gosu redis "$0" "$@"
fi
# 执行传入的命令
exec "$@"
在这个例子中,如果容器以 root 用户启动并执行 redis-server 命令,entrypoint 脚本会自动将进程降级为 redis 用户运行。这是许多官方容器镜像的常见做法。
通过以下命令可验证实际运行的进程用户:
# 启动容器
docker run --name redis-test -d redis
# 查看容器内进程
docker exec redis-test ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
redis 1 0.2 0.1 55344 15080 ? Ssl 10:20 0:00 redis-server *:6379
可以看到,尽管容器是以 root 用户启动的,但实际运行 redis-server 的进程是以 redis 用户运行的。
八、”Permission denied” 错误排查
通过前面分析的 umask 机制、挂载原理和权限管理知识,可以系统性地排查容器中常见的 “Permission denied” 错误。这些错误通常源于文件系统权限、进程用户身份和挂载卷类型之间的不匹配。当遇到 “Permission denied” 错误时,可按照以下步骤进行系统排查:
8.1 确定容器启动账号
首先查看 POD/容器 的配置,确定启动账号:
# 查看 Pod 的 SecurityContext
kubectl get pod security-context-pod -o jsonpath='{.spec.securityContext}'
{"fsGroup":2000,"runAsGroup":2000,"runAsUser":1000}
如果这些设置为空,则默认使用 root 用户(UID 0)。
8.2 确定进程启动账号
接下来,判断实际运行进程的用户:
# 查看容器启动命令
kubectl get pod security-context-pod -o jsonpath='{.spec.containers[*].command}'
["/bin/sh"]
# 如果启动命令为空,可能使用镜像的默认 ENTRYPOINT
kubectl get pod dts-controller -n dts -o jsonpath='{.spec.containers[*].command}'
如果容器使用的是镜像默认的 ENTRYPOINT,需要检查 entrypoint 脚本:
# 查看镜像的 Entrypoint
docker inspect redis:latest -f '{{.Config.Entrypoint}}'
[docker-entrypoint.sh]
# 检查 entrypoint 脚本内容
docker run --rm redis:latest cat /usr/local/bin/docker-entrypoint.sh
8.3 检查目录和文件权限
创建临时容器,以 root 用户检查相关目录和文件的权限:
# 启动临时容器,挂载相关目录
docker run --rm -it --user root -v /path/to/problem/dir:/inspect alpine sh
# 在容器内检查文件权限
ls -la /inspect
8.4 验证权限匹配
最后,确认进程用户是否有权限访问所需的目录和文件:
# 检查进程用户
ps aux | grep [process_name]
# 确认文件权限是否匹配
ls -la /path/to/file
通过这四个步骤,大多数权限问题都能被准确定位和解决。下面通过一个实际案例来展示这些概念的应用。
九、案例分析:Redis Dockerfile
Redis 的官方 Dockerfile 是一个很好的案例,展示了如何在容器环境中正确处理文件权限和用户管理。下面分析 Redis Dockerfile 的关键部分:
9.1 用户创建
Redis 的 Alpine 版本 Dockerfile 中创建了专用的系统用户:
# add our user and group first to make sure their IDs get assigned consistently
RUN set -eux; \
# alpine already has a gid 999, so we'll use the next id
addgroup -S -g 1000 redis; \
adduser -S -G redis -u 999 redis
这段代码创建了一个系统用户 redis(UID 999)和一个系统组 redis(GID 1000),确保了 Redis 进程将使用固定 UID/GID 的非 root 用户运行,提高了安全性。
9.2 权限降级工具安装
Redis 镜像安装 gosu 工具实现权限降级:
# grab gosu for easy step-down from root
ENV GOSU_VERSION 1.17
RUN set -eux; \
apk add --no-cache --virtual .gosu-fetch gnupg; \
arch="$(apk --print-arch)"; \
case "$arch" in \
'x86_64') url='https://github.com/tianon/gosu/releases/download/1.17/gosu-amd64'; sha256='bbc4136d03ab138b1ad66fa4fc051bafc6cc7ffae632b069a53657279a450de3' ;; \
'aarch64') url='https://github.com/tianon/gosu/releases/download/1.17/gosu-arm64'; sha256='c3805a85d17f4454c23d7059bcb97e1ec1af272b90126e79ed002342de08389b' ;; \
# ... 其他架构 ...
esac; \
# ... 下载和验证 gosu ...
chmod +x /usr/local/bin/gosu; \
gosu --version; \
gosu nobody true
这确保了容器可以安全地从 root 用户降级到非 root 用户。
9.3 权限降级实现
在 docker-entrypoint.sh 中实现实际的用户切换:
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
fi
这段代码在以 root 用户启动容器时,会自动将当前目录下的文件所有权调整为 redis 用户,然后切换到 redis 用户继续执行。这正是前面讨论的 “ 容器启动 – 进程账号 “ 部分的实际应用。
9.4 数据目录权限管理
Redis 为数据目录设置了正确的权限:
RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data
这确保了数据目录完全归属于 redis 用户,避免了权限问题。这也呼应了 “ 容器挂载目录权限 “ 部分的讨论。
9.5 umask 设置
在 docker-entrypoint.sh 中,Redis 还实现了精细的 umask 控制:
# set an appropriate umask (if one isn't set already)
# - https://github.com/docker-library/redis/issues/305
# - https://github.com/redis/redis/blob/bb875603fb7ff3f9d19aad906bd45d7db98d9a39/utils/systemd-redis_server.service#L37
um="$(umask)"
if [ "$um" = '0022' ]; then
umask 0077
fi
这将默认的 umask 从 0022 改为 0077,确保新创建的文件只对所有者开放权限,大大提高了安全性。这与 “ 文件/目录创建掩码 “ 部分讨论的内容完全吻合。
通过实际操作可验证 Redis 镜像的这些特性:
# 启动 Redis 容器
docker run --name redis-test -d redis
# 查看容器内进程
docker exec redis-test ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
redis 1 0.2 0.1 55344 15080 ? Ssl 10:20 0:00 redis-server *:6379
# 查看数据目录权限
docker exec redis-test ls -ld /data
drwxr-xr-x 1 redis redis 4096 Jun 14 10:30 /data
# 在容器内创建文件,检查权限
docker exec -it redis-test sh -c "cd /data && touch test-file && ls -l test-file"
-rw------- 1 redis redis 0 Jun 14 10:31 test-file
可以看到,Redis 容器成功地实现了:
- 以非 root 用户运行服务
- 正确设置数据目录权限
- 使用安全的 umask 值(0077)
十、总结
Kubernetes 生态中的文件账号与权限管理涉及多层次的技术细节,从基础的 Linux 权限机制到容器特有的挂载与隔离特性。正确理解 umask 机制、挂载原理和安全上下文设置,是解决权限问题的关键。在实践中,遵循最小权限原则,采用非 root 用户运行容器进程,并为不同场景选择合适的存储卷类型,能有效构建安全稳定的容器化应用,避免常见的 “Permission denied” 等问题。
本文作者 : cyningsun
本文地址 : https://www.cyningsun.com/05-03-2025/files-accounts-and-permissions-under-kubernetes.html
版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!