☰
Linux 特殊权限位详解:SUID、SGID、Sticky 原理与排错
2026/10/2 15:01:37 网站建设 项目流程

上周同事把一个部署脚本chmod 4755之后跑来问我:为什么脚本里访问/etc/shadow还是被拒?我让他把脚本换成 C 编译出来的程序再试,同样的4755,立马就通了。这就是 linux 特殊权限位里最典型的一层误解——suid、sgid、sticky 这三个位不是"更高级的 rwx",它们改的不是"谁能读谁能写",而是进程的身份、新建文件的归属、以及目录里删除动作的判定规则。很多人背得下4755、2775、1777这三个数字,但一旦落到"到底谁的身份生效""为什么脚本上不管用""为什么拷贝之后就失效了",就开始含糊。

我准备把这三条线一次性捋到底:每个位分别在什么对象上生效、内核在哪个环节动的手脚、怎么用八进制和符号两种写法准确设置、怎么在出问题时一步步排查,以及审计和迁移时最容易踩的几个静默坑。文中的演示命令都可以直接在一台测试机上复现,涉及动手的部分我会标清楚前置条件,避免你在生产机上做出不可逆的改动。不管你是刚接触 Linux 权限的运维新人,还是被共享目录折腾过的老手,这篇应该都能留下点能直接抄走的东西。

1. 三个真实的排错现场:SUID、SGID、Sticky 分别卡在哪一步

先从现场说起。脱离了具体现象去背权限位的定义,你永远记不住哪个是哪个,因为这三个位的语义跨度其实挺大:一个改的是进程身份,一个改的是文件归属,一个改的是删除判定。它们唯一的共同点是——都挤在ls -l输出的那九个字符里,用s和t冒充了原本该是x的位置。

1.1 现场一:普通用户改密码,凭什么能写 /etc/shadow

/etc/shadow的权限是000或者640,属主 root,普通用户连读都读不了。但你用普通账号敲passwd,它偏偏就能改自己的密码。答案在于:

$ ls -l /usr/bin/passwd -rwsr-xr-x. 1 root root 32648 3月 10 2022 /usr/bin/passwd

注意属主那一位的x变成了s。这个s就是 SUID(Set User ID,八进制里的4)。它表达的意思是:这个文件被执行时,进程的有效用户 ID(euid)不再是发起者,而是文件属主。passwd的属主是 root,所以它跑起来之后 euid 变成 0,自然写得进/etc/shadow。

这里有个关键细节值得盯住:SUID 改的是"有效身份",不是"真实身份"。进程仍然知道自己是 alice 在操作,只是它现在有权以 root 的身份做事。passwd正是靠这一点——它一边以 root 权限改文件,一边又通过真实身份判断"你只能改你自己的那一条记录"。如果 SUID 把真实身份也一起改掉了,那passwd就会变成一个任人改别人密码的提权工具,早就没人敢用了。

1.2 现场二:共享目录里新建的文件总是跑到别人的组

第二个现场更常见。你给团队建了个/srv/share,属组设成devs,权限775,想着大家都能读写。结果张三在里面存的文件属组是devs,李四存进去的却变成了lisi或者users,同组的人反而写不了。

原因很简单:目录上的权限位只管"能不能在这个目录里创建文件"这个动作,管不了"新文件归哪个组"。新文件的属组默认取创建者的主组。要让它固定继承父目录的属组,就得在目录上打开 SGID(八进制2):

$ chmod 2775 /srv/share $ ls -ld /srv/share drwxrwsr-x. 3 root devs 4096 5月 20 10:12 /srv/share

属组的x变成了s,这就是 SGID 在位。从这个目录里新建的文件和子目录都会自动归到devs组。这一点在团队协作场景里几乎是刚需,也是2775这个数字被反复使用的原因。

1.3 现场三:/tmp 里删不掉别人的文件,到底是哪一位在拦

第三个现场我猜很多人都撞过:/tmp权限是1777,所有人都能写,但你去删同事留下的临时文件时,系统回你一句Operation not permitted。而且更让人困惑的是——那个文件的权限可能是666,你自己明明有写权限。

这就是 Sticky(八进制1)在起作用:

$ ls -ld /tmp drwxrwxrwt. 20 root root 4096 5月 20 10:15 /tmp

最后一位的x变成t。它的规则是:在一个带 sticky 的目录里,只有文件属主、目录属主或者具备相应特权身份的进程,才能删除或重命名目录中的条目。注意,它管的是"删除"和"改名"这两个动作,不管读、不管写。

为什么"删不掉"和"写不了"是两件完全不同的事?因为删除一个文件,本质上修改的是它所在目录的内容(目录项被抹掉),跟你对被删的那个文件本身有没有写权限毫无关系。这个认知上的错位,是绝大多数权限问题的根子。

1.4 三个位的共同点与分水岭

把这三点摆在一起,分水岭就很清楚了:

位八进制作用对象真正改变的东西典型场景
SUID4可执行文件执行时的有效用户 IDpasswd、sudo、mount
SGID2可执行文件 / 目录执行时的有效组 ID / 新条目的属组共享目录、目录树统一属组
Sticky1目录删除与改名的判定条件/tmp、/var/tmp

共同点是三个位都存放在ls -l那九个字符里、都用一位八进制表示;分水岭是它们的生效对象完全不同。SUID 对目录基本没有意义(Linux 上目录的 SUID 位是被忽略的),Sticky 对普通文件在现代 Linux 上也是被忽略的(历史上它表示"把程序常驻内存",那是上古时代的用法)。把作用对象记牢,比记数字管用得多。

2. 把四个数字位算明白:八进制、符号写法与那个容易漏看的大写字母

理解了语义,接下来要解决的是"手别抖"。我见过太多事故是把4755敲成755、把2775敲成2755,或者用chmod +s一次性把两个位都打开了却浑然不觉。权限位这东西没有撤销按钮,改错之后往往要等别人报故障你才发现。

2.1 rwx 只是低三位,第一位是"特殊位打包"

先建立一个坐标系。chmod后面那个八进制数,从右往左看:

  • 第 1 位(最右):other 的权限,r=4、w=2、x=1
  • 第 2 位:group 的权限
  • 第 3 位:user(属主)的权限
  • 第 4 位:特殊位打包,SUID=4、SGID=2、Sticky=1,三个值相加

所以4755拆开就是:特殊位 4(SUID) + 属主 7(rwx) + 属组 5(r-x) + 其他 5(r-x)。2775是 SGID + rwx + rwx + r-x。1777是 Sticky + rwx + rwx + rwx。

三个特殊位是可以叠加的,所以最极端的情况会出现7777(SUID+SGID+Sticky,权限全开)。这种组合在系统里几乎一定能被当成异常项捞出来,正常服务用不到。3755这种"SUID+SGID 一起开"的写法在极少数老程序里存在,但新项目里基本不该出现,因为多开一个位就多一个被利用的面。

2.2 符号写法 chmod u+s / g+s / o+t 与 chmod +s 的差别

八进制写法直白但容易算错,符号写法可读性好但有陷阱。

推荐的方式是明确指定作用对象:

chmod u+s /usr/local/bin/tool # 只加 SUID chmod g+s /srv/share # 只加 SGID chmod o+t /srv/dropbox # 只加 Sticky chmod u-s /usr/local/bin/tool # 去掉 SUID chmod g-s /srv/share # 去掉 SGID chmod o-t /srv/dropbox # 去掉 Sticky

而那个陷阱就是chmod +s 文件——省略了u和g之后,它会把SUID 和 SGID 两个位同时打开。很多人只想加 SUID,敲了个+s,检查时看到属主和属组位置各有一个s才发现设多了。同理,chmod -s 文件会一次清掉两个位。如果你不确定当前状态,改完之后必须用ls -l复核一遍,不要凭记忆判断。

还有一个反直觉的点:加特殊位和加普通位一样受权限约束。非 root 用户只能对自己拥有的文件设置这些位,对别人的文件执行chmod u+s会直接报Operation not permitted。而属于你自己的文件上设 SUID,本质上是"让自己以自己身份运行",没有任何提权效果,所以系统不会拦你,但这也不代表这么做是有意义的。

2.3 ls -l 里 s、S、t、T 的读法

这是全篇最值得反复看的一处细节。ls -l里出现的字母有大小写之分,含义完全不同:

显示形式位置含义
-rws------属主位SUID 已开,且属主有执行权限,位有效
-rwS------属主位SUID 已开,但属主没有执行权限,位形同虚设
----rws---属组位SGID 已开,且属组有执行权限
----rwS---属组位SGID 已开,但属组没有执行权限
drwxrwxrwt其他位Sticky 已开,且其他人有执行权限
drwxrwxrwT其他位Sticky 已开,但其他人没有执行权限

大写S/T的意思是:特殊位本身被置上了,但对应的执行位是关的。对于 SUID 来说,没有执行权限的文件根本不会被执行,SUID 自然不会生效,所以S只是"挂着一个摆设"。这种状态通常是这么来的:先给文件加了 SUID,后来又用某种方式把执行位去掉了;或者先设4644再想做调整。它在审计里很值得关注,因为一个"准备被执行但还差一步"的提权文件,往往意味着有人在调试或者误操作。

对目录来说,Sticky 显示成大写T通常意味着这个目录其他人进不去,那 sticky 的删除保护也就没有实际意义了——因为压根没人能在这个目录里创建或删除东西。

2.4 stat 和八进制输出的交叉验证

用ls -l看字母只能看出"哪个位开了",看不出确切的数字。真正确认时我习惯用stat:

$ stat -c '%a %A %U %G %n' /usr/bin/passwd /srv/share /tmp 4755 -rwsr-xr-x root root /usr/bin/passwd 2775 drwxrwsr-x root devs /srv/share 1777 drwxrwxrwt root root /tmp

%a直接给出八进制权限(含特殊位),%A给出人类可读形式。在写脚本做批量核对时,stat -c '%a'是最省事的,比ls -l | awk稳得多,因为它不受别名、颜色输出和字段对齐的影响。顺便提一句:如果你的ls配置了颜色别名,在脚本里调用时最好写全路径/bin/ls,或者干脆用stat。

3. SUID 只对可执行文件有效:生效链路与三道拦截

SUID 是最容易被神化也最容易被误用的一个位。它的生效链路其实很狭窄:必须在可执行文件上、必须在**执行(execve)**这个动作发生的时候、中间还会有好几道拦截。哪一道没过,表现都是"位设了但没效果",很容易让人怀疑人生。

3.1 内核在 execve 时改的是 euid,不是 ruid

进程在 Linux 里挂着好几个身份标识:真实用户 ID(ruid)、有效用户 ID(euid)、保存的用户 ID(suid)、文件系统用户 ID(fsuid)。权限判定看的是 euid(文件访问还会看 fsuid),而 ruid 决定"你是谁",比如信号发送、passwd判断你改哪条记录,看的都是真实身份。

带 SUID 的文件被execve执行时,内核会把进程的 euid 设置为文件属主的 UID,同时把原来的 euid 保存在 suid 里,方便程序在需要时主动降权。想亲眼看到这个过程,可以起一个 shell 对比:

$ ps -o pid,ruid,euid,suid,comm -p $$ PID RUID EUID SUID COMM 3821 1001 1001 1001 bash $ su - # 以 root 登录后 # ps -o pid,ruid,euid,suid,comm -p $$ PID RUID EUID SUID COMM 3902 0 0 0 bash

想直接看 euid 而手上没有 root 环境时,用一条 Python 就够了:

python3 -c 'import os; print("ruid=%d euid=%d" % (os.getuid(), os.geteuid()))'

用ps的ruid/euid/suid列要注意,老版本 procps 可能不支持这几个字段名,报错时换用 Python 那条更稳。

3.2 五个 UID 和 no_new_privs

在 euid 之外,还有两个容易被忽略的概念。一个是 fsuid,它决定文件访问检查时用哪个身份,绝大多数情况下跟随 euid。另一个是内核的no_new_privs标志:一旦它被设上,execve就不会再因为 SUID 而提升权限——也就是说,在 no_new_privs 生效的进程里,全盘所有 SUID 程序都会变成普通程序。

查看方式很直接:

$ grep NoNewPrivs /proc/self/status NoNewPrivs: 0

手工验证一次 SUID 被压制是什么手感,可以这么干(普通用户就能做):

$ setpriv --no-new-privs /usr/bin/passwd # 或者直接观察一个 SUID 程序在 no_new_privs 下的行为 $ setpriv --no-new-privs python3 -c 'import os; print(os.geteuid())' 1001

这就是为什么在容器、systemd 服务单元、以及某些受限执行环境里,SUID 会"莫名失效"。排查这类问题时,先看NoNewPrivs的值,比盯着文件权限瞎猜高效得多。

3.3 脚本上的 SUID 是无效的:亲手验证一遍

回到开头那个现场。写个脚本加上 SUID:

$ cat > /tmp/whoami.sh <<'EOF' #!/bin/bash echo "ruid=$(id -u) euid=$(python3 -c 'import os;print(os.geteuid())')" EOF $ chmod 4755 /tmp/whoami.sh $ ls -l /tmp/whoami.sh -rwsr-xr-x. 1 alice alice 80 5月 20 10:30 /tmp/whoami.sh

用另一个普通账号跑它,你会发现 euid 还是自己的:SUID 完全没有起到作用。

原因是Linux 内核在处理带#!解释器的脚本时,不会采用脚本文件上的 SUID/SGID 位。真正被加载执行的是解释器(这里是 bash),脚本文件只是被当作参数喂进去,所以进程身份取决于 bash 的属主(root,普通用户当然不能改)。这不是配置问题,是设计上刻意为之——早期允许脚本 SUID 的实现存在可被利用的竞争条件,后来就被彻底关掉了。

这条规则推出来的结论很实用:你要靠 SUID 提权干活,就必须是编译出来的二进制可执行文件(或者 ELF 的某种等价形式),shell、Python、Perl 脚本一律不行。想达到类似效果,正规做法是配sudo的自定义规则,用NOPASSWD限定具体命令和参数,而不是去折腾脚本的 SUID。

3.4 一段最小 C 验证程序:从普通用户读到 root 才读得到的东西

要证明 SUID 真的生效了,最干净的实验是拿/etc/shadow当靶子——普通用户绝对读不到它。写一个只统计行数、不打印内容的程序,避免在终端上暴露任何哈希:

/* countshadow.c */ #include <stdio.h> #include <unistd.h> int main(void) { FILE *fp; char line[1024]; long n = 0; printf("ruid=%d euid=%d\n", getuid(), geteuid()); fp = fopen("/etc/shadow", "r"); if (!fp) { perror("fopen /etc/shadow"); return 1; } while (fgets(line, sizeof(line), fp)) n++; fclose(fp); printf("shadow 行数=%ld\n", n); return 0; }

编译、放置、设位、验证,一条链走完:

gcc -Wall -O2 -o /usr/local/bin/countshadow countshadow.c chown root:root /usr/local/bin/countshadow chmod 0755 /usr/local/bin/countshadow su - alice -c '/usr/local/bin/countshadow' # ruid=1001 euid=1001 # fopen /etc/shadow: Permission denied chmod 4755 /usr/local/bin/countshadow su - alice -c '/usr/local/bin/countshadow' # ruid=1001 euid=0 # shadow 行数=6 chmod u-s /usr/local/bin/countshadow

对照非常明确:同一个程序、同一个用户,只差一个 SUID 位,一次被拒一次通过。做这个实验一定要在测试机或者你自己能随便折腾的虚机上,验证完立刻chmod u-s撤掉。留一个能读/etc/shadow的自制程序在系统里,等于给自己埋了一颗雷,而且它不会出现在任何包管理器的清单里,日后审计时你还得回忆这是谁写的。

3.5 SUID 该用来干什么,不该用来干什么

从系统自带程序里能看出 SUID 的正确用法边界:passwd干的是"改密码"这一件被严格约束的事,sudo做的是"按配置授权",mount在部分发行版上用于让普通用户挂载。它们的共同点是功能单一、输入严格校验、内部还会二次判断真实身份。

反过来,不该用 SUID 的情况也很清楚:

  • 想让整个程序都拿 root 权限跑——这是最危险的用法,一个缓冲区溢出、一个路径注入就变成任意提权
  • 想给脚本提权——内核直接不支持,白折腾
  • 想让某个服务常驻提权——应该用 systemd 的User=/AmbientCapabilities=精确配权,而不是给二进制挂 SUID
  • 想省掉sudo的密码输入——正确做法是配/etc/sudoers.d/里的精细规则,而不是自己写 SUID 包装脚本

现代发行版正在把越来越多功能从 SUID 迁移到文件能力(capabilities)上。最典型的是ping:早些年它是 SUID root,现在是

$ getcap /usr/bin/ping /usr/bin/ping cap_net_raw=ep

只给"构造原始网络包"这一项能力,而不是给整份 root 权限。这是权限设计上的巨大进步,也解释了为什么你在新系统上ls -l /usr/bin/ping看不到s了。后面第 5 节还会回到这个话题,做审计时这两份清单要对着看。

4. SGID 的两种身份与 Sticky 的唯一职责

SUID 讲透了,SGID 和 Sticky 就好理解了。有意思的是 SGID 在可执行文件和目录上表现出的语义完全不同,这也是它最容易被记混的地方。而 Sticky 反而是三个位里职责最单一的——它就管一件事。

4.1 可执行文件上的 SGID:借一个组身份

可执行文件上的 SGID 和 SUID 是同一个套路的兄弟:进程执行时,有效组 ID 变成文件属组的 GID。它的用途比 SUID 窄得多,典型场景是让一个程序以某个特定组的身份访问该组专属的资源。

比如某个程序需要访问属组为backup、权限640的日志文件,就可以把它设成2755并且属组改成backup,程序跑起来之后 eGID 就等于backup,读得了文件。这和 SUID 用的是同一个内核机制,只是改的是组身份那一半。

需要提醒的是:组身份带来的权限,取决于这个组被授予了什么。如果那个组恰好对某些关键目录有写权限,SGID 程序就同样是一把提权钥匙。审计 SUID 程序的时候,顺手把带 SGID 的可执行文件也捞出来,别只盯着 SUID。

4.2 目录上的 SGID:组归属继承,并且能传下去

目录上的 SGID 走的是另一条完全不同的逻辑,也是日常用得最多的一个:在这个目录里新建的文件和子目录,会把属组设置为该目录的属组,而不是创建者的主组。并且新建的子目录会自动带上 SGID,所以这个继承规则能沿着目录树一直传下去。

这一点对团队共享目录意义重大。没有它的时候,你只能靠"让所有人把主组都改成同一个组"这种土办法,而人一旦属于多个组,主组就不一定是什么了,最终结果是一堆乱七八糟的属组。有了它,只要在根节点设一次2775,整棵树就统一了。

有个细节值得强调:属组是继承了,但组权限位仍然受 umask 影响。设了 SGID 的目录里,如果用户的 umask 是022,新建文件会变成644,属组虽然有写权限位,但文件本身根本没给组开写。结果是"组归属对了,但同组的人还是写不了"。解法有两个:把 umask 调成002(要在登录环境里统一改,不能靠临时命令),或者用默认 ACL,这个在第 4.5 节展开。

4.3 Sticky 只管删除和改名,不管读和写

Sticky 的规则简单到一句话就能说完:在带 sticky 位的目录里,只有文件属主、目录属主,或者具备相应特权的进程,才可以删除或重命名目录里的条目。它不限制读取,不限制写入,也不限制在目录里创建新文件。

为什么共享目录需要它?因为一个"所有人可写"的目录天然有个致命问题:如果所有人都能删别人的文件,那这个目录就不能用来放任何重要的东西。/tmp是典型例子——它权限1777,所有人可写,但如果没有 sticky,任何一个本地用户都能把别人的会话文件、socket、临时数据删掉,那就是一个本地的服务拒绝入口。Sticky 存在的意义就是给"人人可写"加一道"仅限自己"的删除限制。

顺带说一个容易被忽略的边界:sticky 不会被新建的子目录继承。在/tmp里创建一个目录,这个新目录的权限是0777 & ~umask的结果,不带 sticky。所以你在/tmp里自己建的目录,里面的文件反而可能被别人删掉。要保护的话,得手动给那个子目录也加上chmod o+t。

这套规则在排查"删不掉文件"时非常好用,判断链路是:

  1. 看目录本身是不是有写权限——没有写权限,谁都删不了(除非是目录属主以外还有特权)
  2. 看目录是否带 sticky——带了就只看"你是不是文件属主或目录属主"
  3. 看文件属主是谁——stat -c '%U %G %n' file一眼确认

顺着这三步走,几乎不用猜。而且要注意我前面提过的那条:删除权限取决于目录,跟文件本身的权限一点关系都没有。一个444的文件,只要你在它所在目录里有写权限且目录没有 sticky,你照样删得掉。

4.4 搭一个真正能用的团队共享目录

把 SGID 和 Sticky 组合起来用,能搭出一个体验相当不错的协作目录。以下是我在几台服务器上反复用过的配置,假设团队组叫devs,成员是 alice 和 bob:

groupadd devs usermod -aG devs alice usermod -aG devs bob mkdir -p /srv/team/{release,docs,tmp} chown -R root:devs /srv/team chmod 2775 /srv/team/release /srv/team/docs chmod 3775 /srv/team/tmp # SGID + Sticky:组内共享,但只删自己的

几个设计取舍值得说明。release和docs用2775:组内所有人可读写,新文件自动归devs,不设 sticky 是因为这两个目录需要互相整理内容。tmp用3775:既有组继承,又限制只能删自己的文件,适合放那些"大家一起看但不想被误删"的中间产物。

如果还希望"新建的文件默认就能被组内成员修改",需要再补一步默认 ACL:

setfacl -d -m g:devs:rwx /srv/team/release setfacl -d -m o::--- /srv/team/release

代价是ls -l的属组位置会多出一个+,而且那里显示的其实是 ACL mask 而不是组的权限位,容易让人误判"组权限怎么变了"。用getfacl看才准确。所以这个方案我一般只用在确实需要"新建即可协作"的目录上,其他情况宁可统一把 umask 调成002来得干净。

4.5 新加的用户组成员身份为什么不生效

这是共享目录场景里出现频率最高的一个"假故障",必须单独拿出来说。你刚执行完usermod -aG devs alice,让 alice 去访问/srv/team,结果还是被拒。原因不是权限配错了,而是Linux 的补充组列表是在会话建立时确定的,已经在跑的 shell 不会自动感知新的组成员关系。

验证和解决都很直接:

# 在 alice 已有的会话里 $ id uid=1001(alice) gid=1001(alice) groups=1001(alice) # 看不到 devs # 重新登录,或者临时切换一次 $ su - alice $ id uid=1001(alice) gid=1001(alice) groups=1001(alice),1002(devs)

newgrp devs也能临时让当前 shell 拿到这个组,但它会另起一个子 shell,环境变量不一定继承完整,只适合用来验证,不适合当作长久方案。遇到"权限明明配了但某人就是访问不了",第一个动作永远是id确认他当前会话里到底有没有这个组,这条经验帮我省过无数次排查。

5. 审计、排查与迁移:特殊权限最容易丢在哪几个环节

把三个位用熟练之后,剩下的活基本就是两类:定期找出系统里有哪些特殊权限文件,以及搞清楚为什么一个明明设好的位在某个环节悄悄没了。第一类靠find和getcap,第二类靠对拷贝、打包、挂载这几条路径的理解。

5.1 用 find 把全盘的特殊权限文件捞出来(注意 -perm - 和 -perm / 的区别)

find -perm的两种写法含义完全不同,这是排查时最容易读错的地方:

写法GNU find 语义BSD/macOS find 语义
-perm -4000所有给定位都置位(SUID 必须有)与 GNU 的/4000相同
-perm /4000任意一个给定位置位要写成+4000
-perm 4000权限恰好等于 4000相同

日常审计我用的命令是这条:

find / -xdev -type f \( -perm -4000 -o -perm -2000 \) \ -printf '%M %u %g %p\n' 2>/dev/null | sort

几个点解释一下。-xdev是关键,它让 find 不跨越文件系统边界,否则会一路爬进/proc、/sys、挂载的网络盘,既慢又吵。-printf '%M ...'输出的是带s的权限字符串,一眼能看出是 SUID 还是 SGID。2>/dev/null挡掉权限不足的报错噪音——如果你需要知道哪些目录没扫到,就别丢错误输出,而是换成-user root之类更聚焦的条件。

想把范围收窄到"最近才出现的",用时间条件是最有效的:

find /usr /opt /tmp /home -xdev -type f -perm -4000 \ -newermt '30 days ago' -printf '%TY-%Tm-%Td %M %p\n' 2>/dev/null

新冒出来的 SUID 文件就是最值得看的东西,因为系统自带的那些几乎都是包管理器装的,不会在你不知情的时候出现。这条命令我在每次系统巡检时都会跑一遍,比全量清单更省注意力。

5.2 和文件能力(capabilities)对一遍账

只盯着 SUID 会漏掉一半的提权面,因为现在很多功能是用文件能力实现的。这两份清单要放在一起看:

getcap -r / 2>/dev/null

输出形如/usr/bin/ping cap_net_raw=ep,ep表示 effective 和 permitted 都置位。一个理解误区是"能力比 SUID 安全得多,所以不用管"——只有在能力足够细的时候才成立。cap_setuid+ep、cap_dac_override、cap_sys_admin这类能力的杀伤力和 SUID root 基本没有区别。所以审计的结论应该统一是:任何能让普通用户获得额外身份的文件,无论走 SUID 还是 capabilities,都要有明确的归属和用途说明,说不清来历的就撤掉。

5.3 拷贝、打包、挂载过程中的静默丢失

这是最让人抓狂的一类问题:源机器上好好的,目标机器上就是不行,而且没有任何报错。常见的几个丢失环节:

环节现象原因对策
cp不带参数(普通用户)新文件没有 suid默认按 umask 重建权限cp -a或cp --preserve=mode,ownership
rsync不带-p权限位被重置-a才包含-p,单用-r不含明确写rsync -aHAX或用-p
tar解包到非 rootsuid 指向自己无法恢复属主为 root以 root 解包,或加-p并配合正确的属主
拷贝到 U 盘 / vfat / exfat全部变成777且无 suid文件系统不存储 Unix 权限用 tar 打包后再拷,保留元数据
NFS / 某些挂载点位设置了但不生效挂载带了nosuidfindmnt -o TARGET,OPTIONS /path检查
容器内suid 程序没反应运行时默认nosuid或启用no-new-privileges用能力或明确的用户配置替代

这里最需要养成肌肉记忆的是findmnt。看到 SUID 程序不生效、ls -l却明明有s,直接查挂载选项:

$ findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS /srv TARGET SOURCE FSTYPE OPTIONS /srv /dev/sdb1 xfs rw,nosuid,nodev,noexec,relatime

nosuid一出现,这个挂载点下所有文件的 SUID/SGID 位都会被内核忽略,无论权限怎么设。同理noexec会让可执行文件根本跑不起来。这两个选项常常出现在/tmp、/var/tmp、U 盘、以及被人为加固过的数据盘上,属于"配置是对的但环境不允许"的典型。

5.4 我平时用的一份核对清单

最后给出我自己在改权限前后会过一遍的清单,基本都是被事故教出来的:

  • 改之前先stat -c '%a %n'记下当前值,别凭记忆回滚
  • 设完立刻ls -l复核字母大小写,大写S/T意味要重新检查执行位
  • 永远不用chmod +s这种省略 who 的写法,明确写u+s或g+s
  • SUID 只加在编译型二进制上,脚本上加了也没用,别浪费时间
  • 目录要 SGID 就用四位数2xxx一次设好,别只改属组忘了位
  • 共享目录同时考虑 sticky,否则"人人可写"等于"人人可删"
  • 涉及组权限的改动完成后,让对方id确认新组已在当前会话生效
  • 验证完的临时 SUID 程序,当场chmod u-s,不要留到"以后再说"

这些条目看起来琐碎,但每一条背后都是一次真实的故障或者一次差点出事。权限管理最麻烦的地方在于它的失败往往不是报错,而是"看起来没问题"。所以与其事后排查,不如在改动的当下就把这几个点确认掉。

我个人在实操中的体会是,把这三个位当成"三件不同的事"来记,比当成"一组特殊权限"来背要牢靠得多:SUID 关心的是进程以谁的名义干活,SGID 在目录上关心的是新东西归谁,Sticky 关心的是目录里的条目能不能被动手。真正出问题的时候,先问自己一句"我现在遇到的是这三件事里的哪一件",答案基本就出来了。另外还有一个小技巧——遇到任何权限相关的怪现象,第一件事永远是stat -c '%a %A %U %G %n'把文件、目录和它的父目录三者一起打出来对比,看清楚每一层到底是谁、什么权限,比翻文档快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询