Kubernetes 下的文件、账号与权限


  1. 一、背景
  2. 二、文件/目录创建掩码
    1. 2.1 umask 的作用
    2. 2.2 权限计算机制
  3. 三、宿主机目录权限
  4. 四、容器挂载目录权限
  5. 五、虚拟文件系统 – 挂载机制
  6. 六、目录挂载权限管理
    1. 6.1 基本权限原理
    2. 6.2 Pod 安全上下文
    3. 6.3 不同卷类型的权限特性
    4. 6.4 新文件创建的权限规则
      1. 6.4.1 emptyDir 和持久卷 (PV) 的新文件
      2. 6.4.2 hostPath 卷的新文件
  7. 七、容器启动 – 进程账号
  8. 八、”Permission denied” 错误排查
    1. 8.1 确定容器启动账号
    2. 8.2 确定进程启动账号
    3. 8.3 检查目录和文件权限
    4. 8.4 验证权限匹配
  9. 九、案例分析:Redis Dockerfile
    1. 9.1 用户创建
    2. 9.2 权限降级工具安装
    3. 9.3 权限降级实现
    4. 9.4 数据目录权限管理
    5. 9.5 umask 设置
  10. 十、总结

一、背景

在容器化环境中,文件权限和用户管理常常会引发各种问题,例如:遇到 “Permission denied” 错误,却不知从何处着手解决。问题背后往往涉及容器文件系统、用户权限和挂载机制等细节。本文将从几个关键问题出发,系统地探讨容器中的文件、账号与权限管理,以更好地理解和解决这些问题。

  1. 进程创建的目录、文件,默认权限是怎么指定的?
  2. 宿主机不存在的路径,是由谁来创建,权限是怎样的?
  3. 容器的挂载路径中不存在的目录是由谁来创建的,权限是怎样的?
  4. 虚拟文件系统是怎么实现挂载、挂载目录读写?
  5. 同一个文件系统 (目录) 挂载到不同的容器 (机器),权限是怎么管理的?
  6. root 启动的容器,进程启动用户一定是 root 么?
  7. “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(虚拟文件系统) 层的实现。

Kubernetes 下的文件、账号与权限-20250503232727-1.png

  • 目录游走:目录游走是逐渐实例化该组件对应的 inode 和 dentry 的过程。在没有任何缓存的情况下,dentry 会先被初始化,在 dentry 中包含文件/目录名字符串。在具体某一级目录中,会调用该目录 inode 的 lookup() 函数查找该目录中的对应子项(子目录或子文件),然后完成子项 dentry 和 inode 的初始化

Kubernetes 下的文件、账号与权限-20250503232727-2.png

  • 挂载点初始化:涉及挂载的关键信息的初始化在挂载的时候就已经完成。即,为源目录添加挂载点标记,同时添加挂载信息(包括,源和目标文件系统的信息)到挂载点列表

  • 挂载点游走:在目录游走时,如果发现该目录标记为挂载点,则从挂载点列表寻找目标文件系统的信息,然后从目标文件系统继续往下遍历

这种挂载机制是理解不同类型的卷在权限处理上存在差异的基础。当目录被挂载后,访问该目录的进程实际上会穿过挂载点,访问到目标文件系统上的内容,而权限检查则会基于目标文件系统上的权限设置。

有了这些挂载机制的基础知识,接下来可以进一步分析 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 的处理机制存在显著差异,这主要与它们的存储实现原理、安全模型和权限管理方式有关:

  1. 持久卷(PersistentVolume,如 AWS EBS、Azure Disk、NFS 等)

    • 支持 fsGroup: 对大多数类型的 PV,fsGroup 设置生效
    • 原因
      • 存储驱动支持:持久卷通常由云服务商或分布式存储系统提供,其存储驱动支持 Kubernetes 的所有权和权限动态修改
      • 多租户隔离需求:PV 是集群级别的资源,设计上需要支持多租户场景,确保不同用户或 Pod 挂载同一卷时能通过 fsGroup 自动隔离权限
    • 生效机制
      • 挂载时自动递归修改卷内所有文件和目录的组所有权为 fsGroup
      • 确保组权限生效,例如设置目录的 setgid 位,使新创建的文件继承父目录的组所有权
      • 若同一 PV 被多个 Pod 挂载,后挂载的 Pod 的 fsGroup 会覆盖之前的设置
    • 持久性fsGroup 的修改是持久的,即使 Pod 删除后也会保留
  2. 临时卷(emptyDir)

    • 支持 fsGroup: fsGroup 对 emptyDir 卷生效
    • 原因
      • emptyDir 卷是 Pod 级别的临时存储,完全受 Kubernetes 控制
      • 每次 Pod 创建时都会新建,无需考虑多租户权限冲突
    • 生效机制
      • 卷创建时自动应用 fsGroup 设置
      • 确保 Pod 中的所有容器都能通过组权限访问卷内文件
    • 临时性:Pod 删除时卷内容被清除,权限问题不会持续存在
  3. hostPath 卷

    • 不支持 fsGroup: fsGroup 设置对 hostPath 卷不生效
    • 原因
      • 直接绑定宿主机文件系统:hostPath 卷直接挂载宿主机上的目录或文件,权限完全依赖宿主机现有设置
      • 安全限制:Kubernetes 设计上避免自动修改宿主机文件系统权限,防止因权限篡改引发安全风险
      • 存储驱动不支持:hostPath 卷的实现不包含动态修改文件组所有权的逻辑
    • 权限行为
      • 挂载后,容器内访问的目录权限与宿主机目录完全一致,不会触发任何所有权或权限修改
      • 如需修改宿主机上的文件权限,需要通过初始化容器或其他机制手动设置
    • 权限协调:若容器进程用户需要访问 hostPath 目录,必须手动设置宿主机目录的权限

6.4 新文件创建的权限规则

当 Pod 内进程创建新文件时,权限规则根据卷类型有所不同:

6.4.1 emptyDir 和持久卷 (PV) 的新文件

当进程在 emptyDir 卷或支持 fsGroup 的持久卷上创建新文件时:

  1. 文件所有者:为创建进程的 UID(受 runAsUser 影响)
  2. 文件组:为 Pod 的 fsGroup 值,不受进程主组影响
  3. 权限位:由基准权限减去 umask 值计算得出
    • 文件基准权限:0666 (rw-rw-rw-)
    • 目录基准权限:0777 (rwxrwxrwx)
    • 常见 umask 为 0022,则新文件权限为 0644 (rw-r--r--)

6.4.2 hostPath 卷的新文件

当进程在 hostPath 卷上创建新文件时:

  1. 文件所有者:为创建进程的 UID(受 runAsUser 影响)
  2. 文件组:为创建进程的主组 ID(受 runAsGroup 影响),不会应用 Pod 的 fsGroup
  3. 权限位:同样受 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 容器成功地实现了:

  1. 以非 root 用户运行服务
  2. 正确设置数据目录权限
  3. 使用安全的 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 许可协议。转载请注明出处!

# Kubernetes