Linux 中的文件权限:chmod、chown 和 umask

1. 总览

Permission denied 错误是在 Linux 上部署应用时最常见的错误之一。应用无法写入日志,无法读取配置文件,或者机密文件被任何用户都能读取。

本文说明 Linux 文件权限模型以及如何使用三个主要工具:chmod 修改权限、chown 修改所有者、umask 控制新文件的默认权限。文章最后,你将能够读取任何文件的权限,分配所需的适当权限,并从 Java 操作 POSIX 权限。

2. 使用 ls -l 读取文件权限

从一个示例目录开始:

mkdir -p shop && cd shop
echo "id,total" > order.csv
mkdir reports
ls -l
-rw-r--r-- 1 an dev 9    Oct 13 09:00 order.csv
drwxr-xr-x 2 an dev 4096 Oct 13 09:00 reports

第一列有 10 个字符:

  • 第 1 个字符是类型:- 表示普通文件,d 表示目录,l 表示符号链接。
  • 其余 9 个字符分成三组,每组 3 个字符:user(所有者)、group(组)、other(其他人)。
  • 每组包含 r(读)、w(写)、x(执行)。- 表示没有该权限。

第三列和第四列分别是所有者(an)和组(dev)。因此,对于 order.csv,an 可以读写,dev 组的成员只能读,其他用户也只能读。

要查看数字形式的权限,可以使用 stat:

stat -c '%a %A %U:%G %n' order.csv reports
644 -rw-r--r-- an:dev order.csv
755 drwxr-xr-x an:dev reports

内核按顺序检查权限。如果你是所有者,只有 user 权限组适用。如果你不是所有者但属于该组,只有 group 权限组适用。其他情况使用 other。各组不能累加。因此,一个权限为 ---r--r-- 的文件会使所有者无法读取,尽管其他人都可以读取。

3. 使用 chmod 修改权限

3.1. 符号形式

符号形式的语法是 [ugoa][+-=][rwxX]。其中 u 是 user,g 是 group,o 是 other,a 是 all。

chmod g+w order.csv      # 为 group 添加写权限
chmod o-r order.csv      # 删除 other 的读权限
chmod u=rw,go= order.csv # user: rw,group 和 other:无
stat -c '%a %A' order.csv
600 -rw-------

运算符 + 和 - 只添加或删除所述权限,其他权限保持不变。运算符 = 重新分配整个组。符号形式很安全,当你只想修改一部分而不需要知道当前权限时。

3.2. 八进制数字形式

每个权限有一个值:r = 4、w = 2、x = 1。将这些值相加可以得到每个组的一个数字。

数字 权限 用途
7 rwx 目录或脚本的所有者
6 rw- 数据文件的所有者
5 r-x 读取并进入目录,运行脚本
4 r-- 仅读
0 --- 无权限

例如,创建一个脚本和一个包含密码的文件:

printf '#!/bin/bash\necho "Backup orders..."\n' > backup.sh
echo "db.password=s3cret" > secret.properties

chmod 755 backup.sh
chmod 600 secret.properties
./backup.sh
stat -c '%a %n' backup.sh secret.properties
Backup orders...
755 backup.sh
600 secret.properties

755 允许任何人运行脚本,但只有所有者可以修改它。600 使得只有所有者可以读写机密文件。数字形式一次性分配所有权限,因此当你知道确切的所需状态时很合适。

3.3. 递归修改权限和大写字母 X

一个经典错误是运行以下命令:

chmod -R 755 shop/

此命令将执行权限分配给所有文件,包括 order.csv。正确的方法是使用大写 X。此符号仅为目录添加 x,或为已经在某个组中有 x 的文件添加:

chmod -R u=rwX,go=rX shop/

结果是目录权限为 755,普通文件权限为 644,而 backup.sh 保持 755 权限。如果需要更严格的控制,可以使用 find 分别处理:

find shop -type d -exec chmod 750 {} +
find shop -type f -exec chmod 640 {} +

4. 目录权限与文件权限的差异

对于目录,rwx 具有不同的含义:

  • r:列出内部项目的名称(ls)。
  • w:在内部创建、删除、重命名项目。此权限必须与 x 一起才能生效。
  • x:进入目录(cd)并访问内部项目(如果已知名称)。

一个意想不到的后果是:删除文件取决于目录的权限,而不是文件的权限。

mkdir inbox && echo "data" > inbox/invoice.txt
chmod 444 inbox/invoice.txt   # 只读文件
rm -f inbox/invoice.txt       # 仍然可以删除,因为 inbox 有 w 权限
ls inbox                      # 空

相反,如果删除目录上的 x 权限,即使文件有 644 权限,你也无法读取文件:

echo "data" > inbox/invoice.txt
chmod 644 inbox/invoice.txt
chmod u-x inbox
cat inbox/invoice.txt
cat: inbox/invoice.txt: Permission denied

当应用报告 Permission denied 且文件权限看起来正确时,请检查路径上的所有父目录。命令 namei -l /path/to/file 列出每个级别的权限。继续之前,使用 chmod u+x inbox 为目录恢复权限。

5. 使用 chown 和 chgrp 修改所有者

chown 修改所有者、组或两者:

sudo chown appuser order.csv          # 仅修改所有者
sudo chown appuser:dev order.csv      # 同时修改所有者和组
sudo chown :dev order.csv             # 仅修改组
sudo chown -R appuser:appuser /opt/shop

需要记住几点:

  • 只有 root 可以修改所有者。普通用户无法将自己的文件“转让”给他人。
  • 普通用户可以更改自己拥有的文件的组,但只能更改为自己是成员的组。使用的命令是 chgrp dev order.csv。
  • chown -R 遍历目录树。当遇到符号链接时,默认会修改链接指向的文件的所有者。如果要修改链接本身的所有者,添加 -h。

部署时常见的模式是服务以专有用户运行,而运维团队共享一个组:

sudo chown -R appuser:ops /var/log/shop
sudo chmod -R u=rwX,g=rX,o= /var/log/shop

通过此设置,服务可以写入日志,ops 组可以读取日志,其他用户无法访问。

6. 使用 umask 设置默认权限

创建新文件时,程序通常请求文件的 666 权限和目录的 777 权限。然后内核清除 umask 中的位。结果是:

实际权限 = 请求权限 AND (NOT umask)
umask            # 查看当前值
umask 027        # 为当前 shell 修改
touch customer.csv && mkdir archive
stat -c '%a %n' customer.csv archive
0022
640 customer.csv
750 archive

使用 umask 027:文件收到 666 减去 027 得到 640,目录收到 777 减去 027 得到 750。Other 完全失去权限,group 失去写权限。

注意: umask 是位屏蔽操作,不是算术减法。使用 umask 033,新文件的权限是 644,而不是 633。原因是位 x 本来不在 666 中,所以没有什么要清除的。

一些常见的值:

umask 文件 目录 何时使用
022 644 755 大多数发行版的默认值
027 640 750 服务器,仅在组内共享
077 600 700 敏感数据

umask 命令仅在当前进程及其子进程中有效。要为 systemd 服务设置固定值,可以在单元文件中声明:

[Service]
User=appuser
UMask=0027

7. 三个特殊位:setuid、setgid、sticky

除了 9 个基本位外,Linux 还有三个特殊位。它们被写成八进制形式开头的第四个数字:

  • setuid (4):程序以所有者的权限运行。例如 /usr/bin/passwd 显示为 -rwsr-xr-x。
  • setgid (2):当设置在目录上时,在目录内创建的新文件将继承目录的组。此位对共享工作目录非常有用。
  • sticky (1):当设置在目录上时,只有文件的所有者才能删除该文件。/tmp 使用此位,显示为 drwxrwxrwt。

例如,为 dev 组创建共享目录:

sudo mkdir /srv/shared && sudo chown :dev /srv/shared
sudo chmod 2770 /srv/shared
stat -c '%a %A' /srv/shared
2770 drwxrws---

注意: 不要给自己编写的脚本或文件分配 setuid,除非你真正理解风险。有漏洞的 setuid root 程序是经典的权限提升路径。

8. 从 Java 操作 POSIX 权限

Java NIO 通过 PosixFilePermissions 支持 POSIX 权限。下面的类写入机密文件并确保只有所有者可以读取:

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermissions;

public class SecretWriter {

    public Path writeSecret(Path dir, String content) throws IOException {
        Path file = dir.resolve("secret.properties");
        Files.writeString(file, content);
        Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-------"));
        return file;
    }
}

fromString("rw-------") 将符号字符串转换为 Set<PosixFilePermission>。然后 setPosixFilePermissions 的作用就像 chmod 600 命令。

你可能会尝试将 PosixFilePermissions.asFileAttribute(...) 传递给 Files.createFile。这种方式仍然会应用 umask,就像系统调用 open() 一样。对于敏感文件,在创建后调用 setPosixFilePermissions 以确保结果。更安全的方法是从一开始就以严格的权限创建文件,以避免任何时间窗口让其他人可以读取文件。

使用 JUnit 5 和 AssertJ 进行测试:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermissions;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

class SecretWriterTest {

    @TempDir
    Path tempDir;

    @Test
    void givenSecret_whenWritten_thenOnlyOwnerCanReadAndWrite() throws Exception {
        assumeTrue(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"));

        Path file = new SecretWriter().writeSecret(tempDir, "db.password=s3cret");

        String perms = PosixFilePermissions.toString(Files.getPosixFilePermissions(file));
        assertThat(perms).isEqualTo("rw-------");
        assertThat(Files.readString(file)).contains("db.password");
    }
}

assumeTrue 将跳过 Windows 上的测试,因为 Windows 没有 posix 属性视图。如果缺少这一行,测试将抛出 UnsupportedOperationException。

9. 常见错误

  • 使用 chmod 777 来“快速修复”。 这种方法向机器上的所有用户授予写权限。确定进程以哪个用户运行(ps -o user= -p <pid>),然后只为该用户或组授予权限。
  • 在目录树上运行 chmod -R 755。 此命令将所有文件变成可执行文件。使用大写 X 或如第 3.3 节所示的 find。
  • 仅检查文件权限。 你还需要使用 namei -l 检查父目录。
  • 运行 sudo 然后忘记修改所有者。 由 sudo 创建的文件属于 root,所以以普通用户运行的应用无法写入。应该立即运行 chown。
  • 在 Docker 中挂载卷。 容器内的 UID 可能与主机上的 UID 不同。内核根据 UID 号而不是用户名来检查权限。
  • SSH 拒绝密钥。 如果权限过于宽松,SSH 会忽略 ~/.ssh/authorized_keys 或私钥。目录 ~/.ssh 需要权限 700,私钥需要权限 600。

10. 结论

Linux 权限模型由三个组(user、group、other)和三个权限(r、w、x)组成。这些权限在文件和目录之间具有不同的含义。

使用 chmod 修改权限:当只需修改一部分时使用符号形式,当想分配全部权限时使用数字形式。使用 chown 和 chgrp 将文件分配给正确的用户和组。使用 umask 使新创建的文件从一开始就具有安全的权限。

贯穿始终的原则是授予尽可能少的权限。使用专用用户运行服务,通过组共享,对所有敏感内容保持 other 为 0。

linuxchmodchownumask安全