SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

Linux umask 默认权限:为什么 root 和用户不同

了解 umask 如何从文件和目录的请求权限中清除位。运行 umask、创建对象并读取模式,验证普通用户因缺少目录执行位出现 Permission denied,而 root 行为不同。

Linux 中 umask 的作用

umask 是 Linux 中每个进程都携带的一个数值。它决定该进程创建的每个文件和目录的权限模式。程序在创建对象时向内核请求一组权限。内核会清除掩码指定的每个位,并应用剩余权限。umask 不会授予访问权限,只会从创建程序请求的权限中移除相应位。

umask 的值不是由发行版决定的。它取决于当前使用的账户,以及 shell 的启动方式。在同一台机器上,即使使用标准镜像,这两个因素也可能在同一时刻产生不同结果。因此,第一步不是查阅手册,而是在当前主机上实际测量。

打印当前 shell 中的 umask

umask
umask -S

第一种形式以八进制打印掩码。第二种形式以它允许的权限打印同一个掩码,使用 chmod 接受的符号形式。保留屏幕上的两行输出。下面的所有内容都将与 shell 刚才打印的结果进行比较。

umask 是 shell 内建命令,不是磁盘上的程序。使用 type umask 确认这一点。这很重要,因为内建命令会直接修改 shell 进程本身。独立程序只能修改自己的进程,随后退出并丢失该修改。

创建文件和目录,然后读取其模式

cd "$(mktemp -d)"
touch probe.file
mkdir probe.dir
stat -c '%a %A %n' probe.file probe.dir

%a以八进制显示模式,%A则以 ls -l 使用的 drwxr-xr-x 格式显示相同的模式。将这两行分别与刚才显示的掩码进行比较。掩码中设置的每一位都会从模式中缺失,因为掩码唯一能做的就是清除这些位。如果还不容易读懂 %A 列,应先弄清 drwxr-xr-x 权限字符串

文件和目录的模式彼此不同,原因不是掩码。touch 请求为所有者、组和其他用户设置读和写权限。mkdir 请求为这三类用户都设置读、写和执行权限。同一个掩码会从两个不同的请求中分别扣除相应权限。因此,touch 创建的文件永远不会具有可执行权限,无论掩码是什么值:因为请求中从未包含执行位,而掩码不能重新添加权限位。

( umask a=rwx; touch open.file; stat -c '%a %A %n' open.file )

这些括号会在子 shell 中运行命令,因此其中的更改会随子 shell 结束而消失。此时掩码不要求清除任何权限,stat 仍然显示文件没有执行位。随后再次运行 umask,原始值就会恢复。这说明该设置存在于进程内部,并由子进程继承,而不是存储在磁盘上的某个位置。

目录最能体现执行位被清除后的影响。

( umask a=rw; mkdir noexec.dir; cd noexec.dir )

以普通用户身份运行时,cd 会因 bash: cd: noexec.dir: Permission denied 失败,因为掩码清除了 mkdir 请求的执行位,而没有执行位的目录无法进入。root 会跳过这项检查,因此只有普通账户会出现这个现象。

为什么 root 和您自己的用户看到的 umask 不同

通过另一个账户,以另一种方式启动相同的测量,并将两份输出并排比较。

umask
sudo -i umask

sudo -i 会启动 root 的登录 shell,并在其中运行内置命令。因此,这是另一个账户通过另一条启动路径进入 shell。在标准 Ubuntu 和 Debian 服务器镜像上,这两行可能输出不同的值。两行都正确。每一行都显示了各自启动路径产生的结果。本文其余部分将说明系统的哪一部分产生了这些结果。

哪个文件决定了该值

grep -nE '^(UMASK|USERGROUPS_ENAB)' /etc/login.defs
grep -rn pam_umask /etc/pam.d/
grep -rn umask /etc/profile /etc/profile.d/ /etc/bash.bashrc ~/.profile ~/.bashrc 2>/dev/null || echo 'no umask line in the shell startup files'

如果第一次 grep 没有输出,请删除 ^ 锚点后再次运行:该行可能被注释掉了,而注释行是文档说明,不是配置。第三条 grep 才是最容易让人意外的地方。在 Debian 和 Ubuntu 中,随系统提供的 /etc/profile 通常主要指向 PAM,而不是自行设置掩码,因此您以为负责该设置的文件往往并不负责。grep 未匹配任何内容时会以非零状态退出,这就是该行末尾使用 || echo 的原因:如果某个镜像的启动文件中没有提到掩码,您会看到消息而不是一片空白,而这条消息本身就是结果。

/etc/login.defs advertises a value and PAM applies one

The UMASK line in /etc/login.defs is the value most guides quote. Neither the kernel nor the shell reads that file. It is read by pam_umask, a PAM (pluggable authentication modules) module that runs when a session is created. pam_umask takes the first value it finds: a umask= entry in the user's GECOS field, then a umask= argument written on the pam_umask.so line itself, then UMASK from /etc/login.defs. Distributions patch this module, so run man pam_umask on your own image and read the order printed there.

That is how /etc/login.defs can advertise one value while your session ends up with another, with no warning printed on either side. The greps tell you which case you are in. If the pam_umask.so line carries its own umask= argument, the login.defs line lost.

USERGROUPS_ENAB 和 root 例外

id -un
id -gn

如果这两个命令输出相同的名称,说明您使用的是用户私有组:创建该账户时,同时创建了一个以用户名命名的专属组。pam_umask 具有用户组行为,该行为由 USERGROUPS_ENAB(位于 /etc/login.defs)控制。启用该行为后,如果账户不是 root,且主组名称与用户名匹配,该模块会将掩码中的所有者位复制到组位。这样,会话最终使用的掩码会为该账户创建的所有对象保留组权限位。root 由模块本身排除在外,而这一排除是同一台服务器上的两个 shell 输出不同掩码的最常见单一原因。

这条规则的依据是,私有组只有一个成员,因此组可写只等同于所有者可写,不会带来额外影响。一旦有人向该组添加第二个成员,情况就会改变。从那一刻起,该成员可以写入该账户过去创建的所有文件,而且无需对这些文件运行任何命令即可获得这种权限。为每项服务分配专用的最小权限用户账户,并有意让该组始终只包含一个成员。

登录 shell、非登录 shell 和非交互 shell

umask
bash -lc 'umask'
bash -c 'umask'

创建会话时,PAM 才会运行:login 在控制台登录时,sshdsusudo -i。一个 shell 启动另一个 shell 时,PAM 不会运行。bash -l 是登录 shell,因此会读取 /etc/profile~/.profile,但它不会调用 pam_umask,因为没有创建会话。bash -c 不读取这两个文件,并继承启动它的进程的掩码。cron 任务、git hook 以及服务管理器启动的程序都属于后一种情况,因此它们使用的掩码取决于父进程的掩码。

因此,“我已在 /etc/profile 中设置了它,但服务写入的模式仍然不正确”是很常见的问题。服务根本没有读取该文件。

设置位置必须确保重启后仍然生效

应在工作负载实际启动的位置设置掩码,因为不同的启动路径会读取不同的文件。

  1. 对于登录的账户:在 /etc/login.defs 中设置 UMASK,由 pam_umask 应用于本机上的每个会话。该设置作用于整台机器,因此会同时影响所有账户。
  2. 对于单个账户:pam_umask.so 行中的 umask= 参数同样作用于整台机器,因此,单个用户的值应放在该用户的 GECOS 字段中;对于登录 shell,使用 ~/.profile;对于交互式 shell,使用 ~/.bashrc
  3. 对于由 systemd 管理的 daemon:在单元的 [Service] 部分设置 UMask=。单元由服务管理器启动,因此不会读取 /etc/profile,pam_umask 也不会运行。在这些文件中,只有单元文件中的设置会传递给 daemon。
  4. 对于由 cron 或钩子启动的脚本:在第一行显式设置 umask,并且必须位于脚本创建任何内容之前。
[Service]
UMask=<the octal mask you chose>

然后从同一启动路径执行一次全新的启动来验证,不要在编辑文件的 shell 中验证。当前 shell 已经保存了自己的掩码,编辑配置文件不会反向影响正在运行的进程。

bash -lc 'umask'
sudo -i umask

为什么事后运行 chmod 不是同一种修复方式

chmod只能修复已经存在的文件。掩码决定尚不存在的文件所采用的权限模式。在目录上运行 chmod -R 后,服务下一次写入的新文件仍会恢复为原来的权限模式,因为该模式来自创建进程,目录本身不会改变它。

此外还存在时间窗口。从文件创建到运行 chmod 之间,文件会以更宽松的权限模式保存在磁盘上,任何能够读取该目录的进程都可以打开它。对于私钥或备份归档,这正是您试图消除的风险窗口。

应在创建时设置权限模式。install -m u=rw,go= newfile /etc/app/newfile 会以明确的权限模式写入目标文件,mkdir -m 对目录执行相同操作。两者都会采用您指定的权限模式,并忽略掩码。ssh-keygen 会为它写入的私钥设置权限模式,因此在其他文件权限都不正确的服务器上,这一个文件通常仍然是正确的。

SSH 通常最容易受到影响。使用普通 mkdir 创建的 ~/.ssh,或使用 cat >> 追加的 authorized_keys,都会采用 shell 的掩码。启用 StrictModes 后,如果密钥文件所在目录允许组写入,sshd 会拒绝读取该文件。客户端会收到 Permission denied (publickey),而服务器上的 /var/log/auth.log 会记录实际原因:

Authentication refused: bad ownership or modes for directory /home/deploy/.ssh

这项检查是有意设计的,而 加固 VPS 上的 SSH 依赖于它正常发挥作用。在新服务器上创建将要使用它的账户之前,应先测量掩码,并结合 新 VPS 上线后的前十分钟 一起完成。这样,这些账户写入的每个文件的权限模式都能提前确定。

复制和归档会忽略掩码

cp -prsync -a 会恢复源文件中记录的权限模式,因此掩码不会影响结果。tar 在 root 身份下提取文件时也会执行相同操作;普通用户使用 -p 时同样如此。从备份恢复的文件会保留创建备份时的权限模式。请先确认这一点,再判断正确的掩码是否被忽略:对于恢复的数据,系统从未查询过掩码。

FAQ

为什么我的 cron 任务创建的文件,其权限模式与 ssh 会话不同?

cron 任务不是登录会话,因此不会运行 pam_umask,也不会读取 /etc/profile~/.profile。它会继承启动该任务的进程的掩码。在脚本第一行显式加入 umask,并确保位于创建任何内容之前;然后在任务内部打印一次掩码,以查看该任务实际使用的值,而不是当前 shell 使用的值。

为什么 /etc/login.defs 中的设置与 shell 输出的值不同?

/etc/login.defs 中的 UMASK 只是 pam_umask 使用的最后备用值。该模块会优先使用用户 GECOS 字段中的 umask= 条目,然后使用 /etc/pam.d/pam_umask.so 行上的 umask= 参数。由 USERGROUPS_ENAB 启用的 usergroups 行为还会重写组数字:对于 primary group 以用户名命名的非 root 账户,组数字会被修改。运行 grep -rn pam_umask /etc/pam.d/id -un; id -gn,查看哪些设置适用于您的账户。

umask 能让文件具有执行权限吗?

不能。掩码只能清除创建程序请求的权限位。touch 从不请求执行位,因此任何掩码都不能生成可执行文件。使用 ( umask a=rwx; touch f; stat -c '%a %A' f ) 在临时目录中验证这一点。要获得执行位,您需要使用 chmod,或者使用创建文件时会请求执行位的程序,例如 install -m

应该在哪里为 systemd 服务设置 umask?

在 unit 的 [Service] 部分中使用 UMask= 设置。服务由服务管理器启动,而不是由登录过程启动,因此不会读取 shell 启动文件,也不会运行 pam_umask。执行 systemctl daemon-reload 并重启该 unit 后,从外部确认结果:让服务创建一个文件,然后使用 stat -c '%a %n' 读取结果。

默认使用允许组写入的掩码是否安全?

当该组只有一个成员时是安全的,这正是用户私有组方案的前提。将第二个账户加入该组后,第一个账户创建的所有文件会立即允许新成员写入,无需对这些文件执行任何命令。运行 id -unid -gn:如果打印出相同的名称,说明您使用的是私有组。如果多个账户共享一个组,请设置一个会清除组写入位的掩码,然后创建文件并读取 stat -c '%a %n',确认更改已生效。

#umask#permissions#pam#login-defs#linux