Appearance
05|权限管理
把一份新生成的 SSH 私钥复制到服务器上,准备登录另一台机器:
bash
ssh -i ~/.ssh/id_rsa deploy@10.0.0.5返回的却是一行红字:
Permissions 0644 for '/root/.ssh/id_rsa' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.私钥正确、路径正确、账号也正确,但 ssh 客户端拒绝使用这个文件——原因藏在文件权限上。本讲围绕 Linux 的权限系统,从最基础的 UGO 模型一路讲到扩展属性,每一节都配一段可以直接照抄到终端里执行的演示,最后回到开篇这个报错并把它修复掉。
一、UGO 权限模型
Linux 的文件访问控制以 UGO 模型 为核心。每个文件同时记录三组权限,对应三类身份:
| 身份 | 含义 |
|---|---|
| User(owner) | 文件的所有者 |
| Group | 文件所属组的成员 |
| Other | 既不是 owner、也不属于该组的所有其他人 |
先在终端里看一眼真实文件长什么样:
bash
ls -l /etc/passwd
# -rw-r--r-- 1 root root 2841 May 20 10:22 /etc/passwd把第一列 -rw-r--r-- 逐位拆开:
| 位置 | 内容 | 含义 |
|---|---|---|
| 第 1 位 | - | 文件类型,- 是普通文件,d 是目录,l 是符号链接 |
| 第 2–4 位 | rw- | owner(root)的权限:可读、可写、不可执行 |
| 第 5–7 位 | r-- | group(root 组)的权限:仅可读 |
| 第 8–10 位 | r-- | other 的权限:仅可读 |
后面的 root root 分别是 owner 和 group。
权限判定顺序:命中即停
进程访问文件时,内核按 owner → group → other 的顺序判定权限,命中即停,不会继续往后比对。
这条规则带来一个新手容易掉进去的陷阱:当某账号同时是文件的 owner 又是文件所属组的成员,且 owner 权限位比 group 更严时,最终生效的是 owner 权限位。
立刻动手验证一下:
bash
# 准备一个测试文件
echo "hello" > /tmp/perm-test.txt
# 把权限设为 owner 只读、group 可读写
chmod 460 /tmp/perm-test.txt
ls -l /tmp/perm-test.txt
# -r--rw---- 1 当前用户 当前用户主组 6 ... /tmp/perm-test.txt
# 当前用户既是 owner 也在 group 里,尝试写入
echo "更多内容" >> /tmp/perm-test.txt
# 输出:bash: /tmp/perm-test.txt: Permission denied虽然 group 权限位是 rw-,但由于当前用户匹配上了 owner,内核只看 owner 的 r--,写入被拒绝。
路径每一级都需要 x 权限
访问文件还有一条更隐蔽的规则:从根目录到目标文件的路径上,每一级目录都必须对当前用户有 x(执行)权限。任何一级缺 x,整条路径就走不通,目标文件本身权限再宽也无济于事。
照下面的步骤演示一次:
bash
# 建一个嵌套目录和一个 777 的文件
mkdir -p /tmp/dir-a/dir-b
echo "可以读吗?" > /tmp/dir-a/dir-b/file.txt
chmod 777 /tmp/dir-a/dir-b/file.txt
# 故意去掉中间目录的 x 权限
chmod o-x /tmp/dir-a
# 切换到一个非 owner 用户尝试访问(如果系统里有 nobody)
sudo -u nobody cat /tmp/dir-a/dir-b/file.txt
# 输出:cat: /tmp/dir-a/dir-b/file.txt: Permission denied文件本身是 777,但因为 /tmp/dir-a 对 other 没有 x,nobody 进不去这个目录,自然也碰不到下面的文件。
清理:
bash
chmod o+x /tmp/dir-a
rm -rf /tmp/dir-ar/w/x 在文件和目录上的语义差异
同样三个字母,作用于文件和作用于目录的含义不同:
| 权限 | 作用于文件 | 作用于目录 |
|---|---|---|
r(读) | 查看文件内容(cat、less) | 列出目录条目(ls) |
w(写) | 修改文件内容 | 在目录中创建、删除、重命名文件 |
x(执行) | 将文件作为程序运行 | 进入目录(cd) |
特别需要注意:目录的 w 权限决定了能否删除其中的文件,与被删除文件自身的权限无关。也就是说,对目录有 w 的用户可以删掉目录里任何文件,哪怕这个文件是别人创建的、文件本身没给你任何权限。
/tmp 是这条规则的典型代价场景,所有用户都能在里面建文件,理论上也都能删别人的文件。Linux 通过 Sticky Bit 限制这一行为,后面第六节会演示。
二、chmod 修改权限
chmod 有两种用法,数字模式和符号模式,按场景挑顺手的用。
数字模式
三位八进制数对应三组权限。每位的取值靠 r=4、w=2、x=1 相加得到:
| 八进制 | 组合 | 权限含义 |
|---|---|---|
| 7 | 4+2+1 | 读+写+执行 |
| 6 | 4+2 | 读+写 |
| 5 | 4+1 | 读+执行 |
| 4 | 4 | 仅读 |
| 0 | — | 无任何权限 |
直接动手感受几组常用值:
bash
# 准备测试目录
mkdir /tmp/chmod-demo && cd /tmp/chmod-demo
touch a.txt b.sh c.key
chmod 644 a.txt && ls -l a.txt # -rw-r--r-- 普通文档
chmod 755 b.sh && ls -l b.sh # -rwxr-xr-x 可执行脚本
chmod 600 c.key && ls -l c.key # -rw------- 仅 owner 可读写
# 清理
cd / && rm -rf /tmp/chmod-demo记住 644(普通文件)、755(可执行脚本/目录)、600(敏感文件)这三个值,日常 90% 的场景够用。
符号模式
符号模式以增量方式调整权限,不用做八进制运算:
bash
chmod u+x script.sh # 给 owner 加执行权限
chmod g+w shared-dir # 给 group 加写权限
chmod o-r file.txt # 去掉 other 的读权限
chmod a+x deploy.sh # 三类身份都加执行(a = all)身份缩写:u(user/owner)、g(group)、o(other)、a(all)。操作符:+ 增加、- 移除、= 精确设置(会覆盖原有权限)。
跑一遍看效果:
bash
touch /tmp/symbol-demo.txt
chmod 600 /tmp/symbol-demo.txt && ls -l /tmp/symbol-demo.txt
# -rw-------
chmod g+r /tmp/symbol-demo.txt && ls -l /tmp/symbol-demo.txt
# -rw-r-----
chmod o=r /tmp/symbol-demo.txt && ls -l /tmp/symbol-demo.txt
# -rw-r--r--
rm /tmp/symbol-demo.txt敏感文件的权限红线
SSH 私钥、TLS 私钥、含密码的配置文件必须收紧到 600,这是某些服务的硬性校验,不是建议。
OpenSSH 客户端用私钥前会检查文件权限,一旦发现 group 或 other 有读权限,直接拒绝使用并报错。下面的步骤把开篇那个报错从头到尾复现一遍:
bash
# 第 1 步:生成一对实验密钥
ssh-keygen -t ed25519 -N "" -f /tmp/lab_key
ls -l /tmp/lab_key
# 默认就是 -rw-------,权限正常
# 第 2 步:故意把权限放宽到 644
chmod 644 /tmp/lab_key
ls -l /tmp/lab_key
# -rw-r--r--
# 第 3 步:尝试用这个私钥连本机
ssh -i /tmp/lab_key -o StrictHostKeyChecking=no nobody@127.0.0.1
# 看到:Permissions 0644 for '/tmp/lab_key' are too open.
# This private key will be ignored.
# 第 4 步:把权限收回 600,再连一次
chmod 600 /tmp/lab_key
ssh -i /tmp/lab_key -o StrictHostKeyChecking=no nobody@127.0.0.1
# 这次不再有权限警告,认证失败的原因会变成密码不对等其他理由
# 说明 ssh 已经成功读取了私钥
# 第 5 步:清理
rm -f /tmp/lab_key /tmp/lab_key.pub这套流程在排查"明明私钥没动过却突然不能登录"时非常有用——大概率不是密钥坏了,是权限被某次操作意外改宽了。
三、chown 修改归属
chown(change owner)修改文件的所有者和所属组,格式 chown owner[:group] path:
bash
chown nginx:nginx /var/log/nginx/access.log # 同时改 owner 和 group
chown app /opt/app/run.sh # 只改 owner
chown :web /var/www/html/index.html # 只改 group
chown -R app:app /opt/app # 递归改整个目录树-R 递归威力很大,路径写错会把整棵子树的归属一锅端走。对 /etc、/var 这种系统目录执行错误的递归 chown,可能直接让大量系统服务起不来。
养成执行递归操作前先做两步确认的习惯:
bash
pwd # 1. 确认当前所在目录
ls -ld /opt/app # 2. 确认目标路径存在且权限符合预期
chown -R app:app /opt/app # 3. 确认无误后再执行四、umask 与新建文件的默认权限
umask 决定新建文件或目录时默认会被扣掉哪些权限位。它不直接指定权限,而是从理论最大权限中减去这个掩码。
计算规则两条:
- 新建文件的理论最大权限是
666(rw-rw-rw-);Linux 出于安全考虑,新建文件默认不带执行权限。 - 新建目录的理论最大权限是
777(rwxrwxrwx)。
常用 umask 对照:
| umask | 新建文件实际权限 | 新建目录实际权限 |
|---|---|---|
022 | 644(rw-r--r--) | 755(rwxr-xr-x) |
027 | 640(rw-r-----) | 750(rwxr-x---) |
077 | 600(rw-------) | 700(rwx------) |
亲手验证一遍:
bash
# 查看当前 umask
umask
# 大多数发行版默认 0022
# 临时改成 077,再创建文件和目录看看
umask 077
touch /tmp/u-file
mkdir /tmp/u-dir
ls -l /tmp/u-file
# -rw-------
ls -ld /tmp/u-dir
# drwx------
# 改回 022 并清理
umask 022
rm -rf /tmp/u-file /tmp/u-dirumask 命令只在当前 shell 及其子进程里生效。要让它持久生效,需要写到 /etc/profile、~/.bashrc,或者对应服务的 systemd unit 里。
服务进程创建出的日志文件权限不对劲时(比如其他账号读不了),除了排查程序代码里的权限设置,也要看看启动这个服务的用户 umask 是多少。
五、ACL:UGO 之外的细粒度授权
UGO 只能表达三类身份。当你需要给单个用户或非主属组开例外,UGO 无法表达,这时候用 ACL(Access Control List)。
举个场景:文件 report.txt 归属 app:app,权限 640。现在要给账号 deploy 单独加一个只读权限,但又不想动文件原有的 owner、group 和权限位。
直接做一遍:
bash
# 准备文件
sudo touch /tmp/report.txt
sudo chown app:app /tmp/report.txt 2>/dev/null || sudo chown root:root /tmp/report.txt
sudo chmod 640 /tmp/report.txt
# 给 nobody 加只读 ACL(用 nobody 是因为它一般默认存在)
sudo setfacl -m u:nobody:r /tmp/report.txt
# 查看完整 ACL
getfacl /tmp/report.txt
# 输出里会出现 user:nobody:r--
# ls -l 此时会在权限位末尾多一个 + 号作为提示
ls -l /tmp/report.txt
# -rw-r-----+ 1 ... /tmp/report.txt
# 删掉 nobody 的 ACL 条目
sudo setfacl -x u:nobody /tmp/report.txt
# 或者一次清空所有 ACL
sudo setfacl -b /tmp/report.txt
# 清理
sudo rm /tmp/report.txtls -l 只会用一个 + 提醒你 "这个文件有额外 ACL",但不会展示具体内容,必须 getfacl 才能看全。
建议把 ACL 留给少量例外场景。系统里一旦 ACL 用得到处都是,后续排查权限问题时很容易漏掉藏在 ACL 里的授权,长期维护成本会陡增。能用 UGO 解决的优先用 UGO。
六、特殊权限位:SUID、SGID、Sticky Bit
这三个特殊权限位在 ls -l 输出里会替换掉原本的 x 位置,显示成 s 或 t。
SUID(Set UID):执行时临时获得 owner 权限
设了 SUID 的可执行文件,任何用户运行它时,进程会临时获得文件 owner 的权限而不是执行者的权限。
最经典的例子是 /usr/bin/passwd:
bash
ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 ...
# ↑ owner 的 x 位变成 s,表示已设 SUID普通用户改密码需要写 /etc/shadow,而 /etc/shadow 只有 root 能写。passwd 通过 SUID 实现了"执行期间以 root 身份运行",改完密码进程退出,权限随之释放。
手动设置和移除 SUID:
bash
chmod u+s /path/to/file # 加 SUID
chmod u-s /path/to/file # 去掉 SUIDSUID 文件本质上是合法的提权通道,必须严格审计。系统里出现的非自带 SUID 可执行文件,是攻击者植入后门的常见手段。可以用下面这条命令把全盘 SUID 文件列出来对账:
bash
sudo find / -perm -4000 -type f 2>/dev/nullSGID(Set GID):目录里新建文件继承目录属组
SGID 设在目录上时,该目录下新建的文件会自动继承目录所属组,而不是创建者的主组。
适合多人协作的共享目录,举个完整流程:
bash
# 建共享目录,归属 ops 组
sudo groupadd ops 2>/dev/null
sudo mkdir /data/shared
sudo chown root:ops /data/shared
sudo chmod 2775 /data/shared # 2 表示 SGID,775 是常规权限
ls -ld /data/shared
# drwxrwsr-x 2 root ops ... /data/shared
# ↑ group 的 x 位变成 s
# 任何人在这里建的文件,组都会是 ops,方便组内成员共享
sudo touch /data/shared/test
ls -l /data/shared/test
# -rw-r--r-- 1 root ops ... test也可以用符号模式:chmod g+s /data/shared。
Sticky Bit:限制目录里的删除行为
Sticky Bit 设在目录上时,只有文件 owner、目录 owner 和 root 能删除目录里的文件,其他用户即便对目录有 w 权限也删不了别人的文件。
bash
ls -ld /tmp
# drwxrwxrwt 18 root root 4096 ...
# ↑ other 的 x 位变成 t,表示 Sticky Bit 已设/tmp 就是 Sticky Bit 的经典应用——所有人都能在里面建临时文件,但谁也不能删别人的。
给自己的目录加 Sticky Bit:
bash
chmod +t /data/shared
ls -ld /data/shared
# 末尾的 x 变成 t七、chattr 与文件系统级扩展属性
chattr 修改的属性作用于文件系统层面,比普通权限位更底层,root 的常规读写权限都管不了它。
immutable:不可修改
bash
sudo touch /tmp/locked.conf
sudo chattr +i /tmp/locked.conf
lsattr /tmp/locked.conf
# ----i--------e----- /tmp/locked.conf
# 即便 root 也改不了
sudo rm /tmp/locked.conf
# rm: cannot remove '/tmp/locked.conf': Operation not permitted
sudo bash -c 'echo x > /tmp/locked.conf'
# bash: /tmp/locked.conf: Operation not permitted
# 必须先去掉 +i 才能修改
sudo chattr -i /tmp/locked.conf
sudo rm /tmp/locked.conf适合保护关键配置文件,攻击者拿到 root 也改不了你的核心配置(除非他知道 chattr 这一招)。
append only:只能追加
bash
sudo chattr +a /var/log/audit.log设了 +a 之后,文件只能被追加,已写入部分覆盖不了也删不掉,适合保护审计日志这类不允许被篡改的文件。
排查提示
当出现 "以 root 身份仍然改不动某文件",并且文件权限、属主、上层目录权限都没问题时,下一个就该看 lsattr,确认文件是不是被设了 +i 或 +a。这类问题在自动化部署脚本里偶尔会卡住——前任运维做过临时防篡改后忘了撤销,新一轮发布就跑不通了。