跳到主要内容

文件系统与权限管理

1. 概念与原理

Linux 文件系统的核心抽象是 一切皆文件。普通文件、目录、设备、管道、socket,甚至进程信息(/proc/<pid>/)都通过文件系统接口暴露。理解这一点,你才能理解为什么 cat /proc/cpuinfo 能看 CPU 信息,为什么 /dev/null 能吞掉输出。

1.1 inode 与文件名

Linux 文件的真实身份不是文件名,是 inode(索引节点)。inode 存储元数据:所有者、权限、大小、时间戳、数据块位置。文件名只是指向 inode 的引用(硬链接)。

# 查看 inode 号
ls -i package.json
# 12345678 package.json

# 同一个 inode 多个名字(硬链接)
ln package.json package-link.json
ls -i package*.json
# 12345678 package.json
# 12345678 package-link.json

这解释了几个常见现象:

  • 删除文件本质是删除文件名(dentry),inode 引用计数为 0 才真正释放空间
  • 一个文件被进程打开后再删除,磁盘空间不会立即释放(直到进程关闭文件描述符)—— 这是日志文件被删但磁盘没释放的根因,必须重启进程或截断 : > /var/log/app.log
  • df 显示空间满了,但 du 算出来文件总和远小于磁盘容量?十有八九是有进程持有已删除文件的 fd,用 lsof | grep deleted

1.2 文件类型

ls -l /
# drwxr-xr-x bin ← d 目录
# -rw-r--r-- hosts ← - 普通文件
# lrwxrwxrwx etc ← l 符号链接
# crw-rw-rw- null ← c 字符设备
# brw-rw---- sda ← b 块设备
# srw-rw-rw- sock ← s socket
# prw-r--r-- fifo ← p 管道

前端日常只关心 -(普通文件)、d(目录)、l(软链接)。node_modules 里 npm 用大量软链接做 hoist,这是 find node_modules -type l 能找到的。

2. 权限模型

2.1 三组九位权限

-rwxr-xr-- 1 alice staff 1024 Jun 18 10:00 deploy.sh
│└┬┘└┬┘└┬┘ │ └─┬─┘ └─┬─┘
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ 所属组
│ │ │ │ │ └──────── 所有者
│ │ │ │ └──────────── 硬链接数
│ │ │ └──────────────── other(其他用户)r--
│ │ └─────────────────── group(同组用户)r-x
│ └────────────────────── owner(所有者)rwx
└──────────────────────── 文件类型

权限数字算法(八进制):

  • r = 4w = 2x = 1
  • rwx = 7rw- = 6r-x = 5r-- = 4

前端必知组合:

  • 644(rw-r--r--)— 普通配置、源码、HTML/CSS/JS,所有人可读,只所有者可写
  • 755(rwxr-xr-x)— 目录、可执行脚本、二进制,所有人可读可进入,只所有者可改
  • 600(rw-------)— 私钥(~/.ssh/id_rsa)、token 文件,只所有者可读写
  • 700(rwx------)— ~/.ssh/ 目录、敏感目录
  • 777(rwxrwxrwx)— 永远不要在生产用,等于把权限完全放开

2.2 目录权限的特殊语义

目录的 rwx 含义和文件不同,这是新人最容易栽的坑:

文件目录
r读取内容列出目录里的文件名(ls
w修改内容创建/删除/重命名目录内文件
x执行进入目录(cd)、访问目录内文件

关键推论:

  • 一个目录只有 r 没有 x,你能 ls 看到文件名但 cat 任何文件都报 Permission denied
  • 一个文件你有 rwx,但所在目录只有 r-x,你能改文件内容,但不能删除它(删除是改目录)
  • nginx 给静态文件目录的标准权限是 755,文件 644

2.3 chmod 三种写法

# 数字模式(最常用)
chmod 644 file
chmod 755 deploy.sh

# 符号模式
chmod u+x deploy.sh # 给所有者加可执行
chmod go-w file # 去掉 group 和 other 的写权限
chmod a+r file # 所有人可读(a = all)

# 递归
chmod -R 755 dist/ # 慎用,会把所有文件都改成 755(含 644 的)
# 更安全的写法:区分文件和目录
find dist -type d -exec chmod 755 {} \;
find dist -type f -exec chmod 644 {} \;

2.4 chown 改所有者

chown alice file # 改所有者
chown alice:staff file # 同时改所有者和组
chown :staff file # 只改组(等同 chgrp)
chown -R nginx:nginx /var/www # 递归

前端高频场景:把 dist/ 部署到 nginx 静态目录后,所有者必须是 nginx 进程的用户(通常是 www-datanginx),否则浏览器拿到 403。

scp -r dist/ user@server:/var/www/app/
ssh user@server "sudo chown -R nginx:nginx /var/www/app && sudo chmod -R 755 /var/www/app"

3. SUID / SGID / Sticky Bit

特殊权限位,前端不常配置但要看得懂。

ls -l /usr/bin/passwd
# -rwsr-xr-x ← s 在 owner 的 x 位 = SUID
含义
SUID(4xxx,s 在 owner x 位)执行时临时获得文件所有者权限,典型例子 passwd —— 普通用户执行它会临时获得 root 权限去改 /etc/shadow
SGID(2xxx,s 在 group x 位)用在目录上:目录内新建的文件自动继承目录的所属组
Sticky Bit(1xxx,t 在 other x 位)用在共享目录上:只有文件所有者能删除自己的文件,/tmp 必有
# 设置
chmod 4755 file # SUID
chmod 2755 dir # SGID
chmod 1777 dir # Sticky

安全警示:任何 SUID 二进制都是攻击面。find / -perm -4000 2>/dev/null 列出系统所有 SUID 文件,新增 SUID 是入侵者的常见后门手法。

4. ACL(访问控制列表)

标准 rwx 只能给所有者、组、其他三类设权限。需要给特定用户单独授权时,用 ACL。

# 给特定用户读写权限
setfacl -m u:alice:rw file
setfacl -m u:bob:rx file

# 查看
getfacl file
# user::rw-
# user:alice:rw-
# user:bob:r-x
# group::r--
# mask::rw-
# other::r--

# 递归 + 默认 ACL(新文件继承)
setfacl -R -m u:nginx:rx /var/www/app
setfacl -R -d -m u:nginx:rx /var/www/app

前端实战场景:CI 部署用户和 nginx 运行用户不是同一个,CI 写文件后 nginx 没权限读。两种解法:

  1. 部署完 chown -R nginx:nginx
  2. 用 ACL 给 nginx 用户加读权限,不改所有者

5. umask

新建文件/目录的默认权限由 umask 控制。

umask # 查看,通常是 0022
# 新文件权限 = 666 - 022 = 644
# 新目录权限 = 777 - 022 = 755

如果生产服务器看到新建文件是 666(rw-rw-rw-),多半是 umask 被改成了 000,是严重安全问题。Dockerfile 里有时为了图方便会写 umask 000,必须避免。

6. 生产实践

6.1 SSH 密钥与权限

~/.ssh/ 权限错了会导致 SSH 连不上,且不会有清晰报错(OpenSSH 静默拒绝):

chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa.pub # 公钥也建议 600 或 644
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/config

调试 SSH 连不上:ssh -vvv user@server 2>&1 | grep -i permission,看是哪个文件权限不对。

6.2 部署目录的标准权限

前端项目部署到 /var/www/myapp/

# 目录 755,文件 644,所有者 nginx
sudo chown -R nginx:nginx /var/www/myapp
sudo find /var/www/myapp -type d -exec chmod 755 {} \;
sudo find /var/www/myapp -type f -exec chmod 644 {} \;

如果应用要写日志/缓存(如 Next.js 的 .next/cache),那个目录单独 chmod 775chown 给应用运行用户。

6.3 容器内的权限陷阱

宿主机挂载目录到容器(-v $(pwd):/app),容器内进程通常以 root 跑,写出来的文件在宿主机上属于 root,宿主机普通用户改不了。

解法:

# Dockerfile 里创建非 root 用户并切换
RUN addgroup -g 1000 node && adduser -D -u 1000 -G node node
USER node

或者 docker run 时指定 uid:

docker run -u $(id -u):$(id -g) -v $(pwd):/app myimage

Node 官方镜像自带 node 用户(uid 1000),直接 USER node 即可。

7. 故障排查

7.1 Permission denied 排查思路

# 1. 当前用户是谁
whoami
id # 看 uid、gid、所有组

# 2. 文件权限和所有者
ls -la <path>
stat <path> # 更详细,含 SELinux 标签

# 3. 整条路径上每个目录都要有 x 权限
namei -l /var/www/myapp/index.html
# f: /var/www/myapp/index.html
# drwxr-xr-x root root /
# drwxr-xr-x root root var
# drwxr-xr-x root root www
# drwx------ root root myapp ← 这里被拦
# -rw-r--r-- root root index.html

# 4. SELinux / AppArmor 也可能拦(CentOS/RHEL 默认开 SELinux)
getenforce # Enforcing 表示开启
ls -Z file # 看 SELinux 标签
# 临时关:setenforce 0

7.2 磁盘满了但 du 算不出来

df -h # 显示满了
du -sh /var # 算出来远小于实际占用

# 找已删除但被进程持有的文件
sudo lsof | grep deleted | sort -k 7 -h | tail
# 输出会有 deleted 标记和文件大小

# 杀掉持有进程或重启服务释放空间

典型场景:Node 应用日志没 rotate,文件被删但进程还在写,磁盘空间不释放。

7.3 文件能看到但 cat 报错

99% 是上级目录缺 x 权限。namei -l 一查就知道。

8. 常见反模式

  • chmod -R 777 /var/www:永远不要。这是把所有人写权限全开,等于邀请别人篡改你的网站。正确做法是 644/755 + chown
  • 以 root 跑 Node 应用:被 RCE 后直接 root 权限沦陷整台机器。Dockerfile 必须 USER 非 root
  • 私钥不设 600:SSH 会直接拒绝,更严重的是同机器其他用户能读你的私钥
  • umask 设成 000:所有新文件都是 666,被同机器其他用户写入是分分钟的事
  • chown -R 误改 /:经典事故,恢复极其困难。任何 -R 操作前先 echo 一遍命令确认路径
  • 生产文件用 vim 直接改 + 不留备份:改坏了恢复不了。先 cp file file.bak.$(date +%Y%m%d)

9. 延伸阅读