[apue] linux 文件访问权限那些事儿

Posted goodcitizen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[apue] linux 文件访问权限那些事儿相关的知识,希望对你有一定的参考价值。

前言

说到 linux 上的文件权限,其实我们在说两个实体,一是文件,二是进程。一个进程能不能访问一个文件,其实由三部分内容决定:

  1. 文件的所有者、所在的组;
  2. 文件对所有者、组用户、其它用户设置的权限访问位;
  3. 启动进程的用户、所在的组、有效用户、有效用户组。

下面先简单说明一下这些基本概念,最后再说明它们是如何相互作用并影响进程访问文件的。

用户与组

用户 ID 唯一标识一个登录用户,记录在口令文件 (/etc/passwd) 中。ID 为 0 的用户为超级用户或根用户 (root),具有绕过文件权限检查的特权。

组 ID 用于将一类用户组织在一起,记录在组文件 (/etc/group) 中。下面这段 shell 脚本用来演示如何创建用户并将它们添加到组中:

 1 #! /bin/bash
 2 useradd lippman
 3 useradd steven
 4 useradd caveman
 5 useradd paperman
 6 echo "create user ok"
 7 
 8 groupadd men
 9 groupadd share
10 echo "create group ok"
11 
12 usermod -a -G share lippman
13 usermod -a -G share steven
14 usermod -a -G men lippman
15 usermod -a -G men caveman
16 usermod -a -G men paperman
17 echo "add user to group ok"
18 
19 groups lippman steven caveman paperman
20 echo "show user and their group ok"
21 
22 groupdel men
23 groupdel share
24 echo "delete group ok"
25 
26 groups lippman steven caveman paperman
27 echo "show user and their group ok"
28 
29 userdel lippman
30 userdel steven
31 userdel caveman
32 userdel paperman
33 echo "delete user ok"
34 
35 rm -rf /home/lippman
36 rm -rf /home/steven
37 rm -rf /home/caveman
38 rm -rf /home/paperman
39 echo "remve user home dir ok"

这段脚本需要有管理员权限,请确保当前用户为 root 用户或属于 sudoer 用户组并使用 sudo 运行。下面是脚本的输出:

$ sudo ./user_init.sh
create user ok
create group ok
add user to group ok
lippman : lippman men share
steven : steven share
caveman : caveman men
paperman : paperman men
show user and their group ok
delete group ok
lippman : lippman
steven : steven
caveman : caveman
paperman : paperman
show user and their group ok
delete user ok
remve user home dir ok

在你的机器上执行这段脚本的时候要特别小心,确保不会有同名的用户或组已经存在,否则可能会将数据误删除。特别是删除用户时,用户的工作目录是不会一并删除的,为了防止下次执行脚本时报警 (工作目录已存在),这里同时删除用户的工作目录 (line 35-38)。groups 命令为参数列表中的每个用户罗列它们所在的组,一个用户可以属于多个组,它创建时所在的组称为初始组,其它组称为附加组,一个用户最多可以添加的附加组数量上限可以通过 sysconf (_SC_NGROUPS_MAX) api 获取 (或通过 getconf NGROUPS_MAX 命令获取),在我的机器上这个值是 65536。关于系统限制值,可以参考我之前写的这篇文章:《[apue] 一个快速确定新系统上各类限制值的工具 》。

从上面两组高亮的输出可以看出,附加组是可以先于用户删除的,删除之后用户就不在组中了。useradd 命令创建的用户初始组名称默认同用户名,也可以通过 -g 参数指定一个已存在的组作为初始组,及通过 -G 参数指定一个或多个附加组,这与 usermod 命令的使用方式是相似的:

 1 #! /bin/bash
 2 groupadd men
 3 groupadd share
 4 echo "create group ok"
 5 
 6 useradd lippman -G share,men
 7 useradd -g share steven
 8 useradd -g men caveman
 9 useradd -g men paperman
10 echo "create user ok"
11 
12 groups lippman steven caveman paperman
13 echo "show user and their group ok"
14 
15 groupdel men
16 groupdel share
17 echo "delete group ok"
18 
19 groups lippman steven caveman paperman
20 echo "show user and their group ok"
21 
22 userdel lippman
23 userdel steven
24 userdel caveman
25 userdel paperman
26 echo "delete user ok"
27 
28 rm -rf /home/lippman
29 rm -rf /home/steven
30 rm -rf /home/caveman
31 rm -rf /home/paperman
32 echo "remve user home dir ok"

与之前不同的是,为了在创建用户时指定初始组,组的创建被放在了前面。这段脚本的输出如下:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
groupdel: cannot remove the primary group of user \'caveman\'
groupdel: cannot remove the primary group of user \'steven\'
delete group ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
delete user ok
remve user home dir ok

可以看到通过 -G  添加组时,还是创建了默认的初始组 (lippman),而组在作为用户的初始组存在的情况下是无法被删除的 (line 15-16,附加组可以),所以最好调整一下删除用户和组的顺序:

 1 #! /bin/bash
 2 groupadd men
 3 groupadd share
 4 echo "create group ok"
 5 
 6 useradd lippman -G share,men
 7 useradd -g share steven
 8 useradd -g men caveman
 9 useradd -g men paperman
10 echo "create user ok"
11 
12 groups lippman steven caveman paperman
13 echo "show user and their group ok"
14 
15 userdel lippman
16 userdel steven
17 userdel caveman
18 userdel paperman
19 echo "delete user ok"
20 
21 rm -rf /home/lippman
22 rm -rf /home/steven
23 rm -rf /home/caveman
24 rm -rf /home/paperman
25 echo "remve user home dir ok"
26 
27 groupdel men
28 groupdel share
29 echo "delete group ok"

这样再跑就没问题了:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
delete user ok
remve user home dir ok
delete group ok

还有个有意思的点可以关注一下:

  • 删除用户时,用户的初始组也会被一起删除,但仅限该初始组没有被其它用户共享的情况下;
  • 单独创建的附加组即使没有包含任何用户,也不会随着最后用户的删除被自动删除。

为了简化后面的描述,将使用以下术语表示上面的概念:

  • 超级用户: root
  • 用户 ID:uid (user id)
  • 用户组 ID:gid (group id)
  • 用户初始组 (登录组):initgrp (initial group)
  • 用户附加组:supgrp (supplementary group)

与用户和组相关的一些命令罗列如下:

  • 用户:useradd / usermod / userdel / users
  • 用户密码:passwd / useradd | usermod -p
  • 用户组:groupadd / groupmod / groupdel
  • 用户组密码:gpasswd / groupadd | groupmod -p  (你没看错,用户组也可以有密码)
  • 用户与组的关系:id / groups / groupmems / usermod | useradd -g | G / gpasswd -a | -d
  • 用户登录:su / sudo / who / whoami / last / ac

这里需要强调的是通过 usermod 修改用户组时,有三种方式:

  • usermod -a group1,group2... user:将 group[1-n] 添加到 user 的附加组中,原附加组保持不变;
  • usermod -G group1,group2... user:将 user 的附加组设置为 group[1-n],原附加组被清除;
  • usermod -g group user:将 user 的初始组设置为 group。

另外,像上面例子那样,删除用户时需要同时删除用户 home 目录的时候,只需要给 userdel 添加一个 -r 参数即可。有时除了 home 目录,系统还会为新用户创建邮件目录,如果删除用户不清理这些目录的话,再次创建的同名用户就会告警,这里都可以通过 -r 参数一并删除,避免后顾之忧。

后面我们会用这里创建的用户及组来做一些验证,具体就是在 line 14 插入一些测试脚本,用于验证一些在多用户场景下的权限问题。使用 su 命令可在多用户之间切换,如果用户设置了密码,则在切换时要求输入密码,这里为了测试的便利性,都没有给用户帐号添加密码,在实际场景中应避免这样使用。

文件的用户与组

文件本身有很多类型:

  • 普通文件
  • 目录文件
  • 符号链接
  • 块设备
  • 字符设备
  • FIFO
  • 套接字

所有文件都有创建者的 uid 和 gid,也有对应的文件权限位。针对普通文件,还可以再做一细分:

  • 可执行文件
  • 一般文件

可执行文件一般符合某种固定格式 (例如 elf),是进程的载体。针对这种文件,可以多设置两种标志位:

  • 设置用户 ID
  • 设置组 ID

它们决定了以该文件作为进程启动时,新进程所使用的 uid 和 gid。对于 Solaris 系统,设置组 ID 也可以给普通的一般文件设置,不过含义也大为不同:表示启用强制性文件记录锁,这是一种非标准扩展,不在本文的讨论范围,这里就不再展开说明了。

针对目录文件,也可以多设置两种标志位:

  • 设置组 ID
  • 粘住位 (sticky bit / svtx)

设置组 ID 与文件中的标志位相同,但是作用于目录时,意义又不一样了:表示该组下创建的文件的用户组 ID 将追随自己,而不是创建进程的组 ID,关于进程的组 ID 详见下一节,关于目录设置组 ID 位后新建文件的所有权,详见“新建文件的权限”这一节;目录加入粘住位时,会改变目录默认的删除文件、修改文件名的规则,具体见“进程访问文件时内核权限检查过程”这一节。

为了简化后面的描述,将使用以下术语表示上面的概念:

  • 文件创建者(拥有者)用户 ID:ouid (owner uid)
  • 文件创建者(拥有者)用户组 ID:ogid (owner gid)
  • 文件权限位:perm (permission)
  • 文件设置用户 ID:setuid
  • 文件设置组 ID:setgid
  • 文件粘住位:svtx (saved text bit)

需要注意的是文件没有附加组的概念,它属于上一节"用户与组"的范畴,文件只能属于一个用户组,在创建时确定,不随用户的变更而变更,本节末尾有一个测试用例来验证这一点。以上与文件相关的概念术语对应到文件的 stat 结构体的关系如下:

  • ouid:st_uid
  • ogid:st_gid
  • 文件类型:type = st_mode & S_IFMT
    • 普通文件:type & S_IFREG
    • 目录文件:type & S_IFDIR
    • 符号链接:type & S_IFLNK
    • 块设备:type & S_IFBLK
    • 字符设备:type & S_IFCHR
    • FIFO:type & S_IFFIFO
    • 套接字:type & S_IFSOCK
  • setuid:st_mode & S_ISUID
  • setgid:st_mode & S_ISGID
  • svtx:st_mode & S_ISVTX

这个结构体使用 stat / fstat / lstat 等 api 获取,如果要获取符号链接本身的属性,需要使用 lstat,否则获取的是符号链接指向的目标属性。此外,还可以通过在 find 命令中指定参数来查找特定类型的文件,以上内容与 find 参数之间的对应关系如下:

  • 普通文件:-type f
  • 目录文件:-type d
  • 符号链接:-type l
  • 块设备:-type b
  • 字符设备:-type c
  • FIFO:-type p
  • 套接字:-type s

这些符号简写其实与 ls 输出的文件类型是一致的 (每行第一个字符),关于 ls 的输出例子,请参考后面和 find 结合查找文件的例子。三个额外的标志位通过 chmod 修改时,使用的关键字符如下:

  • setuid:chmod u+/-s
  • setgid:chmod g+/-s
  • svtx:chmod o+/-t

注意额外标志位是与固定的 u/g/o 权限组搭配的,关于权限组请参考“文件访问权限位”一节。如果进行了错误的搭配,虽然不会报错,但是也不会生效。由于这三个标志位是与执行权限放在一起的,所以最终显示什么字符还与之前有没有设置可执行 (x) 权限有关:

  • setuid+x:rws --- ---
  • setuid-x:rwS --- ---
  • setgid+x:--- rws ---
  • setgid-x:--- rwS ---
  • svtx+x : --- --- rwt
  • svtx-x :--- --- rwT

即小写字母表示有执行权限,大写表示没有。也可以使用 find 搜索带有特定标志位的文件,上面的内容与 find 搜索参数的对应关系为:

  • setuid:-perm -u+s
  • setgid:-perm -g+s
  • svtx:-perm -o+t

格式与 chmod 非常类似。其它的 rwx 权限位也都是可以搜索的,这里就不赘述了。下面我们用这个命令在测试机上搜索一些“特殊”的文件,首先看下 setuid 标志位:

$ find / -perm -u+s 2>/dev/null | xargs ls -ldh
-rwsr-xr-x 1 root root     52K Oct 31  2018 /usr/bin/at
-rwsr-xr-x 1 root root     73K Aug  9  2019 /usr/bin/chage
-rws--x--x 1 root root     24K Feb  3 00:31 /usr/bin/chfn
-rws--x--x 1 root root     24K Feb  3 00:31 /usr/bin/chsh
-rwsr-xr-x 1 root root     57K Aug  9  2019 /usr/bin/crontab
-rwsr-xr-x 1 root root     32K Oct 31  2018 /usr/bin/fusermount
-rwsr-xr-x 1 root root     77K Aug  9  2019 /usr/bin/gpasswd
-rwsr-xr-x 1 root root     44K Feb  3 00:31 /usr/bin/mount
-rwsr-xr-x 1 root root     41K Aug  9  2019 /usr/bin/newgrp
-rwsr-xr-x 1 root root     28K Apr  1  2020 /usr/bin/passwd
-rwsr-xr-x 1 root root     24K Apr  1  2020 /usr/bin/pkexec
---s--x--- 1 root stapusr 208K Oct 14  2020 /usr/bin/staprun
-rwsr-xr-x 1 root root     32K Feb  3 00:31 /usr/bin/su
---s--x--x 1 root root    144K Jan 27 05:56 /usr/bin/sudo
-rwsr-xr-x 1 root root     32K Feb  3 00:31 /usr/bin/umount
-rwsr-sr-x 1 abrt abrt     15K Oct  2  2020 /usr/libexec/abrt-action-install-debuginfo-to-abrt-cache
-rwsr-x--- 1 root dbus     57K Sep 30  2020 /usr/libexec/dbus-1/dbus-daemon-launch-helper
-rwsr-xr-x 1 root root     16K Apr  1  2020 /usr/lib/polkit-1/polkit-agent-helper-1
-rwsr-xr-x 1 root root     11K Apr  1  2020 /usr/sbin/pam_timestamp_check
-rwsr-xr-x 1 root root     36K Apr  1  2020 /usr/sbin/unix_chkpwd
-rws--x--x 1 root root     40K Aug  9  2019 /usr/sbin/userhelper
-rwsr-xr-x 1 root root     12K Nov 17  2020 /usr/sbin/usernetctl

搜索到的全是普通文件,且是可执行文件,大部分位于 /usr/bin 下面,一般是超级用户开放给普通用户使用的命令。再看下 setgid 针对普通文件的情况:

$ find / -type f -perm -g+s 2>/dev/null | xargs ls -lh
-r-x--s--x 1 root slocate   40K Apr 11  2018 /usr/bin/locate
---x--s--x 1 root nobody   374K Aug  9  2019 /usr/bin/ssh-agent
-r-xr-sr-x 1 root tty       15K Jun 10  2014 /usr/bin/wall
-rwxr-sr-x 1 root tty       20K Feb  3 00:31 /usr/bin/write
-rwsr-sr-x 1 abrt abrt      15K Oct  2  2020 /usr/libexec/abrt-action-install-debuginfo-to-abrt-cache
---x--s--x 1 root ssh_keys 455K Aug  9  2019 /usr/libexec/openssh/ssh-keysign
-r-x--s--x 1 root utmp      11K Jun 10  2014 /usr/libexec/utempter/utempter
-rwxr-sr-x 1 root root      11K Nov 17  2020 /usr/sbin/netreport
-rwxr-sr-x 1 root postdrop 214K Apr  1  2020 /usr/sbin/postdrop
-rwxr-sr-x 1 root postdrop 258K Apr  1  2020 /usr/sbin/postqueue

情况和 setuid 文件类似,一般是一些特殊的用户组 (slocate / nobody / tty / postdrop …) 开放给普通用户使用的命令。setgid 针对目录时含义完全不同,所以这里限定了查找类型是普通文件,关于 setgid 目录的例子,留到后面再说明。最后顺便从上面的 ls 输出看一下各列含义:

  • 第一列主要是 perm,其中也附带显示一些其它信息:
    • 第一个字母是类型,- 表示普通文件;
    • 之后分别是三组权限位,特殊标志也显示在这里
  • 第二列为硬链接数;
  • 第三列为 ouid;
  • 第四列为 ogid;
  • 第五列为 size,对于目录只表示目录文件本身占用的空间,不代表目录内文件占用总空间,想要显示目录占用总空间,需要使用 du 命令;
  • 第六列为最后修改日期;
  • 第七列为文件名。

使用 -h 选项使用 human readable 方式显示文件大小——添加合适的单位 (K/M/G) 来让人更易读,否则直接显示字节数;使用 -d 选项来打印目录文件本身而不是列出目录下的文件;ls 选项非常多,有兴趣的同学可以自行 man 页查看。

case:file_group_unchanged.sh

这个用例用来验证文件的 ogid 在创建时确定,不随用户所属用户组的变更而变更。它由两部分脚本组成,第一部分脚本中用户将基于现在的组创建文件,在用户切换所属组后,第二部分脚本中将基于新切换的组再创建文件,并分别列出两个文件的详情,通过观察它们的 ogid 来证明原文件的组不变。先来看第一部分脚本:

1 #! /bin/sh
2 echo "switch to user $(whoami)"
3 # ensure new user can create file
4 cd /tmp
5 
6 touch this_is_a_test_file
7 ls -lh this_is_a_test_file

再来看第二部分脚本:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 groups $(whoami) 
 7 echo "show user and their group ok"
 8 
 9 touch this_is_a_demo_file
10 ls -lh this_is_a*
11 
12 rm this_is_a*
13 echo "remove testing file ok"

最后来看框架脚本中添加的部分:

1     # case: file group unchanged
2     cp ./file_group_unchanged_1.sh /tmp/
3     cp ./file_group_unchanged_2.sh /tmp/
4     su - lippman -s /tmp/file_group_unchanged_1.sh
5     # change current owner\'s group
6     usermod -g share lippman
7     su - lippman -s /tmp/file_group_unchanged_2.sh
8     # change group back, otherwise we will got error on delete group
9     usermod -g lippman lippman

其中:

  • cp 命令将脚本放置在所有用户可访问的目录 (默认位置为用户私有工作目录,其它用户一般不能访问),以便下一步做测试;
  • su 命令将以新用户的身份运行脚本,这里使用了 lippman 用户,当然也可以选用其它任何用户,usermod -g 在 root 权限下执行时,可将任意用户的 initgrp 设置为任意已存在的用户组。
  • 夹在两部分之间的脚本 (line 6) 用于切换用户所属的组。

这个用例需要使用两个分开脚本的原因可以罗列如下:

  • 在脚本中调用 usermod 总是报错,提示没有权限 (即使只是将 initgrp 修改为 supgrp 中的一个也是如此);
  • 如果使用 sudo usermod,则需要将 lippman 加入 sudoer 文件才能起作用,但是那样就感觉测试用例的可移植性差一些了;
  • usermod 命令修改用户组之后,用户需要重新登录才能生效,这里每次 su 就相当于一次用户登录。

上面脚本的运行结果如下:

$ sudo ./user_init.sh
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
-rw-r--r-- 1 lippman lippman 0 May 30 21:13 this_is_a_test_file
switch to user lippman
lippman : share men
show user and their group ok
-rw-r--r-- 1 lippman lippman 0 May 30 21:13 this_is_a_test_file
-rw-r--r-- 1 lippman share   0 May 30 21:13 this_is_a_demo_file
remove testing file ok
delete user ok
remve user home dir ok
delete group ok

重点观察新旧文件的 ogid 项,发现用户切换组后,原文件的 ogid 不受影响,和预期的一致。

最后需要补充的一点是,su -s 选项用来基于新用户身份执行一段脚本,而不能直接输入 su username,否则会在脚本中执行过程中弹出交互式子 shell 从而导致执行被中断。

进程的用户与组

进程有比较多的用户和组属性:

  • 实际用户 ID
  • 实际组 ID
  • 有效用户 ID
  • 有效组 ID
  • 附加组 ID
  • 保存的设置用户 ID
  • 保存的设置组 ID

让我从进程的创建开始一一梳理:用户的初始进程是由登录 (login) 程序启动的,它读取 passwd 配置文件中该用户对应的 uid 和 gid,作为用户根进程的实际用户 ID 和实际组 ID,用于标识进程是谁,一般在整个登录会话过程中不会改变,当然超级用户可以改变它们,这个话题超出了文章的范围,放在以后说明。进程的有效用户 ID 和有效组 ID 默认情况下与实际用户 ID 和实际组 ID 一致,只有当出现以下情况时它们才不一致:  

  • 新进程的可执行文件有 setuid 标志且 ouid 与当前用户不同;
  • 新进程的可执行文件有 setgid 标志且 ogid 与当前用户不同。

场景一,进程的有效用户 ID 被设置为可执行文件的 ouid;场景二,进程的有效组 ID 被设置为可执行文件的 ogid;两个标志可以同时存在,亦可以同时生效 (网上有说法只有一个能生效是不对的,请看本节末尾的验证用例)。

有效用户 ID 与有效组 ID 是进程访问文件时内核权限检查的主要依据,具体的检查过程请参考“进程访问文件时内核权限检查过程”这节。

进程的附加组 ID 即启动进程用户的附加用户组 (supgrp),这个作为有效组 ID 的补充手段用于权限校验,附加组 ID 中每个组都与有效组 ID 的作用等价 (即只要有一个附加用户组匹配了文件 ogid,那么对应的权限就会生效)。没有"设置附加组 ID" 这类的东西,所以附加组都是“原汁原味”不会改变的,这一点请看本节最后的验证用例。

以我们耳熟能详的 access 函数为例,它使用的是实际用户 ID 与实际组 ID 进行访问权限检查,而不是有效用户 ID 和有效组 ID,也就是说 access 返回失败的文件,进程并不一定就不能访问,这一点需要注意 (虽然没什么用,因为你也不能确定它可以访问)。书上有一个很好的例子,本节就不再画蛇添足了,在“进程访问文件时内核权限检查过程”这节中你可以看到一个 shell 版本的 demo,演示了相同的功能。

为了简化后面的描述,将使用以下术语表示上面的概念:

  • 进程实际用户 ID:ruid (real uid)
  • 进程实际组 ID:rgid (real gid)
  • 进程有效用户 ID:euid (effective uid)
  • 进程有效组 ID:egid (effective gid)
  • 进程附加组 ID:supgid (supplementary gid)
  • 进程保存的设置用户 ID:save setuid
  • 进程保存的设置组 ID:save setgid

与进程 ID 相关 api 罗列如下:

  • ruid:getuid / setuid / setreuid / getresuid / setresuid
  • rgid:getgid / setgid / setregid / getresuid / setresuid
  • euid:geteuid / seteuid / setreuid / getresgid / setresgid
  • egid:getegid / setegid / setregid / getresgid / setresgid
  • supgid:getgroups / setgroups
  • save setuid:getresuid / setresuid
  • save setgid:getresgid / setresgid

set 部分一般需要严格的权限检查,留在以后介绍进程关系时说明。save setuid / save setgid 和进程运行过程中执行 exec 相关,也不在这里展开。先来看 get 部分,有些 api 可以一次性获取多个 id,所以在一个 ID 后面会跟多种获取途径。一般通过 ps 命令来显示进程的各种 ID:

$ ps -axo pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 1031    81    81    81    81    81    81 -                    /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
 1037   998   997   998   997   998   997 997                  /usr/bin/lsmd -d
 5971   999   998   999   998   999   998 998                  /usr/lib/polkit-1/polkitd --no-debug
12357  1002  1003  1002  1003  1002  1003 1003                 sshd: yunh@pts/0
12465  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
14457    89    89    89    89    89    89 12,89                pickup -l -t unix -u
28301    89    89    89    89    89    89 12,89                qmgr -l -t unix -u
28632     0     0     0     0     0     0 -                    /usr/sbin/rsyslogd -n
28675     0     0     0     0     0     0 -                    /usr/sbin/crond -n
28720   997   995   997   995   997   995 -                    /usr/sbin/chronyd
28839    32    32    32    32    32    32 -                    /sbin/rpcbind -w

上面我们讲的各种 ID 和 ps 命令的 format 参数 (-o) 及标题之间关系如下:

  • ruid:-o ruid / RUID
  • rgid:-o rgid / RGID
  • euid:-o euid / EUID
  • egid:-o egid / EGID
  • supgid:-o supgid / SUPGID
  • save setuid:-o suid / SUID
  • save setgid:-o sgid / SGID

ps 还可以展示许多其它的 ID,和本文关系不大,就不一一罗列了。

case:setuid_setgid_order.sh

这个用例用于验证 setuid 和 setgid 可以同时作用于一个可执行文件,并且最终影响启动的进程。这个例子由三段脚本组成,需要在框架脚本中添加如下代码:

1     # case: setuid setgid order
2     cp ./setugid /tmp/
3     cp ./setuid_setgid_order_1.sh /tmp/
4     cp ./setuid_setgid_order_2.sh /tmp/
5     cp ./setuid_setgid_order_3.sh /tmp/
6     su - lippman -s /tmp/setuid_setgid_order_1.sh
7     su - caveman -s /tmp/setuid_setgid_order_2.sh
8     su - lippman -s /tmp/setuid_setgid_order_3.sh

其中 setugid 是一个可执行文件,启动后 sleep 10 秒然后退出,主要是用来验证启动进程的一些属性,比较简单就不放源码了; line 3-5 将三段脚本复制到公共目录,原因同上;line 6-8 分别启动三个用户去执行脚本。第一个脚本用来准备 setuid / setgid  程序:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 # create setuid/setgid/setuid & setgid program
10 cp setugid setuid_demo 
11 chmod u+s,ugo+wx /tmp/setuid_demo
12 ls -lh setuid_demo
13 
14 cp setugid setgid_demo
15 chmod g+s,ugo+wx setgid_demo
16 ls -lh setgid_demo
17 
18 cp setugid setuid_setgid_demo
19 chmod ug+s,ugo+wx setuid_setgid_demo
20 ls -lh setuid_setgid_demo
21 
22 echo "create testing setuid/setgid file ok"

就是将 setugid 这个程序复制了三份,并分别设置了它们的 setuid / setgid / setuid & setgid 标志位。注意这里使用 id 打印了当前登录用户的各种 ID 值,这个在后面会用到。第二段脚本分别启动三个进程,并打印它们的 ID 值:

 1 #! /bin/sh
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 ./setuid_demo &
10 ./setgid_demo &
11 ./setuid_setgid_demo &
12 
13 echo "start setuid/setgid program ok"
14 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
15 
16 echo "waiting them to exit..."
17 wait

重点就是 line 9-11 了,使用子进程的方式启动,这样可以同步打印 ps 的输出结果 (line 14),在退出这段脚本前使用 wait 等待所有子进程结束。看到这里似乎就足够了,那第三段脚本是用来做什么的呢?答案是清理刚才的可执行文件:

1 #! /bin/sh
2 echo "switch to user $(whoami)"
3 # ensure new user can create file
4 cd /tmp
5 
6 rm setuid_demo
7 rm setgid_demo
8 rm setuid_setgid_demo
9 echo "remove testing file ok"

至于为什么要一个单独的脚本来清理,稍后再说,这里先上脚本的输出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman),1004(men),1005(share)
-rwsrwxrwx 1 lippman lippman 9.4K May 31 20:51 setuid_demo
-rwxrwsrwx 1 lippman lippman 9.4K May 31 20:51 setgid_demo
-rwsrwsrwx 1 lippman lippman 9.4K May 31 20:51 setuid_setgid_demo
create testing setuid/setgid file ok
switch to user caveman
uid=1005(caveman) gid=1004(men) groups=1004(men)
start setuid/setgid program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
16113     0     0     0     0     0     0 0                    sudo ./user_init.sh
16124     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
16261     0     0     0     0     0     0 1004                 su - caveman -s /tmp/setuid_setgid_order_2.sh
16273  1005  1004  1005  1004  1005  1004 1004                 /bin/sh /tmp/setuid_setgid_order_2.sh
16275  1005  1004  1003  1004  1003  1004 1004                 ./setuid_demo
16276  1005  1004  1005  1006  1005  1006 1004                 ./setgid_demo
16277  1005  1004  1003  1006  1003  1006 1004                 ./setuid_setgid_demo
16278  1005  1004  1005  1004  1005  1004 1004                 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supg
waiting them to exit...
16275 exit
16277 exit
16276 exit
Last login: Mon May 31 20:51:09 CST 2021 on pts/0
switch to user lippman
remove testing file ok
delete user ok
remve user home dir ok
delete group ok

首先各个文件的 setuid / setgid 位被正确设置了;其次 ps 的输出可以看到,都是使用 uid / gid 的形式,这里就体现到了 id 命令的重要性,它已经提前将两个用户的 id 打印出来了,可以对号入座了:

  • setuid_demo:euid 1003 为 lippman,egid 1004 为 men;
  • setgid_demo:euid 1005 为 caveman,egid 1006 为 lippman;
  • setuid_setgid_demo:euid 1003 为 lippman,egid 1006 为 lippman;

全使用数字输出可能有点乱,推荐将一个程序的 euid / egid  与它的 ruid / rgid 对比着来看,就能看出区别来:每个标志位都能单独起作用,不存在谁生效了另外一个就不生效的问题。

需要注意的一点就是,不能使用 shell 脚本来充当 demo 进程,为 shell 脚本文件设置 setud / setgid 不会起作用,其实这个细想一下也可以想通——真正启动的进程是 sh / bash 这类实体,shell 脚本文件只是它们解释执行的数据文件。

最后来说为什么要将清理脚本单独列出来,这是因为 caveman 没有删除文件的权限,如果合并到脚本二的话,会导致删除失败,所以有必要切回到创建文件的用户再去删除文件,关于删除文件需要的权限,请参考“文件访问权限位”一节; 关于 svtx 位设置后 (/tmp 目录) 的删除文件权限,请参考“进程访问文件时内核权限检查过程”一节。

case:process_supgid_unchanged.sh

这个用例主要用来验证进程启动后 supgid 不随用户 supgrp 改变而改变。这个例子由一段脚本组成,被用户执行两次,用户在执行期间 supgid 发生了改变。需要在框架脚本中添加如下代码:

1     # case: process groups unchanged
2     cp ./setugid /tmp/
3     cp ./process_supgid_unchanged.sh /tmp/
4     rm /tmp/should_wait 2>/dev/null
5     su - lippman -s /tmp/process_supgid_unchanged.sh
6     # change current owner\'s supplementary group
7     usermod -G lippman lippman
8     touch /tmp/should_wait
9     su - lippman -s /tmp/process_supgid_unchanged.sh

主要分为三步,先以当前附加用户组启动进程 (line 5),然后改变用户的附加进程组 (line 7),最后以新的附加用户组启动进程 (line 9)。通过对比两次启动的进程 supgid 来观察它们的差异。这里以用户身份启动一个脚本的方法与之前相同,不同的是设置了一个标志位文件 /tmp/should_wait 来标识是否需要等待启动的进程,这也是研究了很多方法之后找到的一个解决方案,之前尝试过使用环境变量、用户配置文件 (~/.bash_profile),都达不到期望的效果。下面来看测试脚本的内容:

 1 #! /bin/bash
 2 echo "switch to user $(whoami)"
 3 # ensure new user can create file
 4 cd /tmp
 5 
 6 # show user & group id for later use
 7 id
 8 
 9 ./setugid &
10 echo "start program ok"
11 ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supgid,cmd
12 
13 if [ -f "/tmp/should_wait" ]; then 
14     echo "waiting them to exit..."
15     wait
16 fi

主要就是启动进程 (line 9),打印进程信息 (line 11),这个进程还是复用的上个用例中的 setugid 程序,主要是利用它启动后 sleep 10 秒的时机通过 ps 来观察一些进程的属性。和之前修改用户组一样,修改了用户的附加组信息后,需要用户重新登录才能生效,所以这里需要同样的用户执行两次脚本。在第二次执行时,如果也不等待 demo 子进程结束就退出,会导致删除用户时报错:

userdel: user lippman is currently used by process 4911

而第一次执行时又不能等待子进程 (需要保证旧的进程还运行时修改用户附加组信息),所以这里使用了事先配置好的标志文件 (/tmp/should_wait) 来决定是否等待。下面看下脚本的输出:

$ sudo ./user_init.sh 
create group ok
create user ok
lippman : lippman men share
steven : share
caveman : men
paperman : men
show user and their group ok
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman),1004(men),1005(share)
start program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
 4677     0     0     0     0     0     0 0                    sudo ./user_init.sh
 4688     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
 4892     0     0     0     0     0     0 1004,1005,1006       su - lippman -s /tmp/process_supgid_unchanged
 4909  1003  1006  1003  1006  1003  1006 1004,1005,1006       /bin/bash /tmp/process_supgid_unchanged.sh
 4911  1003  1006  1003  1006  1003  1006 1004,1005,1006       ./setugid
 4912  1003  1006  1003  1006  1003  1006 1004,1005,1006       ps -ao pid,ruid,rgid,euid,egid,suid,sgid,supg
27258  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
28544  1002  1003  1002  1003  1002  1003 1003                 vim user_init.sh
Last login: Tue Jun  1 11:34:10 CST 2021 on pts/1
switch to user lippman
uid=1003(lippman) gid=1006(lippman) groups=1006(lippman)
start program ok
  PID  RUID  RGID  EUID  EGID  SUID  SGID SUPGID               CMD
 3218  1002  1003  1002  1003  1002  1003 1003                 /bin/bash -l
 4677     0     0     0     0     0     0 0                    sudo ./user_init.sh
 4688     0     0     0     0     0     0 0                    /bin/bash ./user_init.sh
 4911  1003  1006  1003  1006  1003  1006 1004,1005,1006       ./setugid
 4923     0     0     0     0     0     0 1006                 su - lippman -s /tmp/process_supgid_unchanged
 4934  1003  1006  1003  1006  1003  1006 1006                 /bin/bash /tmp/process_supgid_unchanged.sh
 4936  1003  

以上是关于[apue] linux 文件访问权限那些事儿的主要内容,如果未能解决你的问题,请参考以下文章

linux关于用户权限的那些事儿

Linux——万字总结用户与组的权限那些事儿!建议收藏!

apue 外传

关于软硬链接那些事儿

#yyds干货盘点# Kubernetes 集群权限管理那些事儿(17)

Linux压缩那些事儿