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。