Linux和Docker的capabilities介绍
转载:https://www.cnblogs.com/charlieroro/p/10108577.html
验证环境:centos7 x86/64 内核版本4.19.9
在linux 2.2版本之前,当内核对进程进行权限验证的时候,可以将进程划分为两类:privileged(UID=0)和unprivilege(UID!=0)。其中privileged的进程拥有所有内核权限,而unprivileged则根据如可执行文件的权限(effective UID, effective GID,supplementary group等)进行判断。Linux内核从2.2版本开始,就加进的Capabilities的概念与机制,并随着版本升高逐步得到改进。
基于文件访问的进程权限控制(RUID EUID和SUID)
此时进程执行主要涉及6个id:Real uid/gid,Effective uid/gid/supplementary group,Saved set-user-ID/saved set-group-ID。
下面分别用RUID, EUID,SUID来表示实际用户ID,有效用户ID,设置用户ID。另外用户ID是个整型数,为了说明方便真接使用了用户名来代表不同的UID。先解释一下这几个ID的作用:
RUID, 用于在系统中标识一个用户是谁,当用户使用用户名和密码成功登录后一个UNIX系统后就唯一确定了他的RUID.
EUID, 用于系统决定用户对系统资源的访问权限,通常情况下等于RUID。
SUID,用于对外权限的开放。跟RUID及EUID是用一个用户绑定不同,它是跟文件而不是跟用户绑定。
下面以不同的user id为例进行讲解,group id也是类似的。
- supplementary group为user的增补组,例如在添加一个名为usetTest1的user时候,-g执行该user的primary group,-G指定该usetTest1的supplementary groups。使用id命令可以看到“gid=”后面对应usetTest1的primary group,“groups=”后面对应usetTest1的supplementary groups。supplementary groups可以用与DAC验证
[root@localhost ~]# groupadd newGrp1
[root@localhost ~]# groupadd newGrp2
[root@localhost ~]# useradd -u 10000 -g root -G newGrp1,newGrp2 userTest1
[root@localhost ~]# su userTest1
[userTest@localhost root]$ id
uid=10000(userTest) gid=0(root) groups=0(root),1001(newGrp1),1002(newGrp2) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
- 进程的RUID为执行该进程的用户ID,该值通常不需要变更;当判断一个进程是否对某个可执行文件有权限时,需要验证EUID,在没有开启SUID功能时,EUID的值等于RUID;SUID主要用于设置EUID。
在上一步中创建了一个usetTest1用户,可以在/etc/passwd查看该用户的home目录,为/home/userTest1
userTest1:x:10000:0::/home/userTest1:/bin/bash
为验证SUID的功能,su切换到userTest1,并在/home/userTest1下创建一个空文件,可以看到wr.log仅对用户userTest1开放写权限
[userTest1@localhost ~]# touch wr.log[userTest1@localhost ~]# ll-rw-r--r--. 1 userTest1 root 10 Dec 13 18:50 wr.log
在/home/userTest1下编译一个小程序,用于查看当前进程的RUID,EUID和SUID,并写入wr.log。可以看到getIds对所有用户开发了可执行权限
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
uid_t ruid;
uid_t euid;
uid_t suid;
getresuid(&ruid, &euid, &suid);
printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d
",ruid,euid,suid);
uid_t rgid;
uid_t egid;
uid_t sgid;
getresgid(&rgid, &egid, &sgid);
printf("real_group_id=%d, effictive_group_id=%d, saved_group_id=%d
",rgid,egid,sgid);
FILE *stream;
stream = fopen( "wr.log", "a+" );
fprintf( stream, "%s", "hello" );
fclose( stream );
return 0;
}
total 20
-rwxr-xr-x. 1 userTest1 root 8712 Dec 13 18:50 getIds
-rw-r--r--. 1 userTest1 root 554 Dec 13 18:50 getIds.c
-rw-r--r--. 1 userTest1 root 10 Dec 13 18:50 wr.log
在userTest1用户下执行getIds,有如下内容,可以看到其UID为10000,跟创建该用户时设置的值是一样的,RUID=EUID;拉起该进程用户所在的group以及该文件所属的group都是root,所以group的数值显示均为0
[userTest1@localhost ~]$ ./getIds
real_user_id=10000, effictive_user_id=10000, saved_user_id=10000
real_group_id=0, effictive_group_id=0, saved_group_id=0
在同一个host上创建不同组的用户userTest2。
[root@localhost ~]# groupadd -g 20001 newGrp3
[root@localhost home]# useradd -u 10001 -g newGrp3 userTest2
切换到用户userTest2,并进入/home/userTest1(可能需要为该目录添加other的rx权限)下执行getIds,但因为wr.log的用户和组是userTest1:root,而当前用户是userTest2:newGrp3,因此会因为无法打开wr.log出现段错误。同时也可以看到当前进程的RUDI=EUID=10001,即创建userTest2时的UID;RGID=EGID=20001,为创建newGrp3时的GID
[userTest2@localhost userTest1]$ ./getIds
real_user_id=10001, effictive_user_id=10001, saved_user_id=10001
real_group_id=20001, effictive_group_id=20001, saved_group_id=20001
Segmentation fault (core dumped)
SUID的作用就是使可执行文件在不同用户下能以文件拥有者的权限去执行。在userTest1用户下为getIds添加SUID,此时getIds文件的权限中user对应的x变为了s
[userTest1@localhost ~]$ chmod 4755 getIds
[userTest1@localhost ~]$ ll
-rwsr-xr-x. 1 userTest1 root 8712 Dec 13 18:50 getIds
-rw-r--r--. 1 userTest1 root 554 Dec 13 18:50 getIds.c
-rw-r--r--. 1 userTest1 root 15 Dec 13 19:02 wr.log
切换到userTest2,执行getIds,此时可以执行成功,RUID没有变,但EUID和SUID变为了userTest1的值,此时EUID被SUID值为了10000。即当前程序使用userTest1的权限(EUID)去写入wr.log,因此不会出错。但使用SUID是有安全风险的,本例中的程序并没有能力影响除了wr.log之外的系统环境,但如果是一个包含很多功能的命令(如mount ip等),对该命令授予使用某个用户的完整权限,很大程度上有权限泄露的风险,因此对文件设置SUID时需要谨慎。
[userTest2@localhost userTest1]$ ./getIds
real_user_id=10001, effictive_user_id=10000, saved_user_id=10000
real_group_id=20001, effictive_group_id=20001, saved_group_id=20001
更多关于RUID EUID和SUID的内容参见深刻理解——real user id, effective user id, saved user id in Linux
使用capabilities解决上述问题
在linux内核2.2版本之后将基于用户的权限进行了划分,称为capabilities,capabilities是线程相关的,使用时需要在线程上进程设置(完整的capabilities介绍参见capabilities)。那么如何以capabilities解决上述的问题呢?一个简单的办法是改变wr.log的用户和组,这样就不会出现权限问题
对getIds.c做一个小改动增加一行修改wr.log的用户和用户组的操作,其中10001为usetTest2对应的UID,20001为userTest2对应的GID
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
uid_t ruid;
uid_t euid;
uid_t suid;
getresuid(&ruid, &euid, &suid);
printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d
",ruid,euid,suid);
uid_t rgid;
uid_t egid;
uid_t sgid;
getresgid(&rgid, &egid, &sgid);
printf("real_group_id=%d, effictive_group_id=%d, saved_group_id=%d
",rgid,egid,sgid);
chown("/home/userTest1/wr.log", 10001, 20001);
FILE *stream;
stream = fopen( "wr.log", "a+" );
fprintf( stream, "%s", "hello" );
fclose( stream );
return 0;
}
编译上述文件,并使用root用户在userTest1目录下设置getIds1拥有修改文件用户和组的权限CAP_CHOWN,+ep代表将该权限添加到capabilities的Effective和Permitted集合中(下面介绍),
[root@localhost userTest1]# setcap cap_chown+ep getIds1
在userTest2下执行getIds可以看到可以执行成功,注意到wr.log的用户和组也被修改为了userTest2的用户和组
[userTest2@localhost userTest1]$ ./getIds1
real_user_id=10001, effictive_user_id=10001, saved_user_id=10001
real_group_id=20001, effictive_group_id=20001, saved_group_id=20001
[userTest2@localhost userTest1]$ ll
-rwxrwxrwx. 1 userTest1 root 8712 Dec 13 18:50 getIds
-rwxr-xr-x. 1 root root 8760 Dec 13 20:08 getIds1-rw-r--r--. 1 userTest2 newGrp3 30 Dec 13 20:09 wr.log
查看getIds的capabilities,可以看到与设置的一样。最终程序能够运行的原理其实是一样的,即程序的EUID和文件的EUID是一样的。
[userTest2@localhost userTest1]$ getcap getIds1
getIds1 = cap_chown+ep
更简单的办法是给chown设置capabilities,这样进程执行的时候会获取chown上的capabilities,这样就可以拥有权限去执行。在host上执行下面命令。切换到userTest2时就可以使用chown命令直接修改用户和组。此处不能通过给bash设置cap_chow capabilities来操作,因此此时是非root用户,bash进程在执行chown命令的时候会丢掉所有capabilities,导致缺少capabilities而无法运行
# setcap cap_chown=eip /bin/chown
capabilities介绍
- capabilities可以分为线程capabilities和文件capabilities。
- 线程capabilities包含以下4个capabilities集合:
- Effective:内核进行线程capabilities检查时实际使用到的集合
- Inheritable:当程序对应的可执行文件设置了inheritable bit位时,调用execve执行该程序会继承调用者的Inheritable集合,并将其加入到permitted集合。但在非root用户下执行execve时,通常不会保留inheritable 集合,可以考虑使用ambient 集合,当一个程序drop掉一个capabilities时,只能通过execve执行SUID置位的程序或者程序的文件带有该capabilities的方式来获得该capabilities
- permitted:effective集合和inheritable集合的超集,限制了它们的范围,因此如果一个capabilities不存在permitted中,是不可以通过cap_set_proc来获取的。当一个线程从permitted集合中丢弃一个capabilities时,只能通过获取程序可执行文件的capabilities或execve一个set-user-ID-root(以root用户权限运行的)程序来获得
- ambient :是在内核4.3之后引入的,用于补充Inheritable使用上的缺陷,ambien集合可以使用函数prctl修改。当程序由于SUID(SGID)bit位而转变UID(GID),或执行带有文件capabilities的程序时会导致该集合被清空
线程可以使用3种方式修改capabilities:
- fork:子进程使用fork后会继承父进程的capabilities
- cap_set_proc:直接调用系统函数修改,但需要CAP_SETPCAP capabilities权限。在内核2.6.33版本之后,禁止程序直接修改非本进程的capabilities,只允许修改调用者自身进程的capabilities(参见capset)。
- execve:使用该函数后的capabilities计算方式如下:
- 文件capabilities和线程capabilities共同决定了执行execve之后线程的capabilities。设置文件capabilities需要有CAP_SETFCAP 权限。文件capabilities有如下3种:
- Effective:为一个标记位,非capabilities集合。如果设置该标记位,执行execve后的新permitted集合中的capabilities都会添加到effective集合中;反之不会添加(参见上述公式中的:P'(effective) = F(effective) ? P'(permitted) : P'(ambient))。
- Inheritable:该集合主要是配合线程capabilities集合使用,具体使用方式参见上述公式
- Permitted:同上
文件的capabilities使用linux 扩展属性来实现(extended attribute,以下简称EA),EA使用命名空间管理,实现方式比较简单,即key-value方式。文件的capabilities保存在EA的security.capability中,security就是一个命名空间。使用setcap给/usr/bin/的ls目录添加一个capabilities,加入ES,IS和PS中。
# setcap cap_net_raw=eip ls
使用getfattr可以导出该文件对应的EA,"-m -"用于导出所有EA,"-e hex"以16进制方式导出EA
# getfattr -d -m - -e hex ls
# file: ls
security.capability=0x0100000200200000002000000000000000000000
security.selinux=0x73797374656d5f753a6f626a6563745f723a62696e5f743a733000
可以看到有2个EA,security.capability对应文件的capabilities,另外一个“security.selinux”主要被linux的安全模块调用,实现强制访问控制(MAC),当然我们可以是使用text方式解码此命名空间的内容,跟使用ls -Z查看的结果是一样的
# getfattr -d -m - -e text ls
# file: ls
security.capability=" 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
security.selinux="system_u:object_r:bin_t:s0"
# ls -Z ls
-rwxr-xr-x. root root system_u:object_r:bin_t:s0 ls
上面使用hex解码出的security.capability值为0x0100000200200000002000000000000000000000,含5个32位的长度,即0x01000002 00200000 00200000 00000000 00000000,对应以下的结构体,注意其为小端序,转化为大端序为0x0200001 00002000 0002000 00000000 00000000
struct vfs_cap_data {
__le32 magic_etc; /* Little endian */
struct {
__le32 permitted; /* Little endian */
__le32 inheritable; /* Little endian */
} data[VFS_CAP_U32];
};
第一个32位数值0x0200001的最后一个bit位为1,该bit位就是文件effective的bit位,linux/capability.h中有如下定义(参见代码)
VFS_CAP_FLAGS_EFFECTIVE 0x000001
在设置文件的capabilities时,内核会根据capabilities的版本(版本的介绍参见capabilities)进行不同处理,同时也会将capabilities 版本号和effective bit位进行位或以及小端序处理(即0x02000000&0x000001=0x0200001),这样就得到了上面的0x0200001。内核代码会根据长度优先处理VFS_CAP_REVISION_2的情况,内核代码参见L501。第三个和第四个32位数据时是一样的,因为添加capabilities时指定了=eip,表示permitted和inheritable,同样也是小端序,2000代表的就是cap_net_raw
#define VFS_CAP_REVISION_2 0x02000000
- 当执行一个capability-dumb binaries(即设置了文件capabilities,但程序本身并没有调用libcap库管理这些capabilities)时,程序会尝试获取所有的文件capabilities,如果获取失败,则返回错误。这主要是为了防止程序缺少某些capabilities而无法运行
写一个小程序验证上述功能,仅用于输出一句话,编译该文件,输出文件名为hello
#include<stdio.h>
int main()
{
printf("hello world
");
return 0;
}
为hello添加容器中不存在的capabilities ,将该hello使用如下命令拷贝到容器中执行,会出现“sh: ./hello: Operation not permitted”的错误,因此hello程序会尝试获取文件的cap_mac_admin,但因为容器的bounding集合中不存在该capabilities而获取失败。
# setcap cap_mac_admin=eip hello# docker cp hello c03a:/home
-
当使用execve执行set-user-ID-root程序时有如下规则:
- 当执行一个set-user-ID-root程序,或程序的RUD或EUID为0时,则该文件的inheritable集合和permitted集合设置为全1
- 当执行一个set-user-ID-root程序,或程序的EUID为0时,则该文件的effective bit设置为1
-
当线程在不同用户之间进行转换时,有如下规则:
-
如果线程的RUID,EUID以及SUID的一个或多个为0,当这些UID的值都转变为非0时,permitted, effective, ambient集合都会被清空
-
当线程的EUID从0转变为非0,则程序的effective集合会被清空
-
当线程的EUID从非0转变为0,则permitted集合会被拷贝到effective集合中如果系统用户的UID从从非0转变为0(setfsuid),则如下capabilities会从effective集合中清除;当系统的EUID从非0转变为0,则permitted集合会被拷贝到effective集合中
-
如果系统UID 从0转变为非0,则如下capabilities会被清空
CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_FOWNER, CAP_FSETID, CAP_LINUX_IMMUTABLE (since Linux 2.6.30), CAP_MAC_OVERRIDE, CAP_MKNOD
- 当调用capset或cap_set_proc设置程序的capabilities时,必须遵守如下规则:
- 如果调用者没有CAP_SETPCAP ,则新的inheritable必须是现有inheritable和permitted的合集的子集
- (Since Linux 2.6.25)新的inheritable必须是现有inheritable和bounding的合集的子集
- 新的permitted必须是现有permitted的子集
- 新的effective集合必须是现有permitted集合的子集
这样也看出如果现有进程中的permitted中不存在某个capabilities,那么即使该进程有CAP_SETPCAP权限,也不能设置该capabilities。
使用如下程序验证上述部分功能,test.c用于设置当前线程(进程)的capabilities,调用execve执行test1,中间使用getchar()来中断程序,用于手动查看当前程序的capabilities;test1仅查看execv执行之后的capabilities,编译时需要加上-lcap选项,链接cap库
test.c //调用者
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#undef _POSIX_SOURCE
#include <sys/capability.h>
extern int errno;
void listCaps(pid_t pid)
{
uid_t ruid;
uid_t euid;
uid_t suid;
getresuid(&ruid, &euid, &suid);
printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d
",ruid,euid,suid);
struct __user_cap_header_struct head={_LINUX_CAPABILITY_VERSION_1, pid};
struct __user_cap_data_struct data={};
capget(&head, &data);
printf("Cap data ES=0x%x, PS=0x%x, IS=0x%x
", data.effective,data.permitted, data.inheritable);
}
int main(int argc, char **argv)
{
int error;
int stat;
pid_t parentPid = getpid();
cap_t caps = cap_init();
cap_value_t capList[] ={ CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_SETPCAP,CAP_SETFCAP } ;
unsigned num_caps = 4;
cap_set_flag(caps, CAP_EFFECTIVE, num_caps, capList, CAP_SET);
cap_set_flag(caps, CAP_INHERITABLE, num_caps, capList, CAP_SET);
cap_set_flag(caps, CAP_PERMITTED, num_caps, capList, CAP_SET);
if (cap_set_proc(caps)) {
perror("capset()");
return EXIT_FAILURE;
}
listCaps(parentPid);
getchar();
char *argv2[]={"test1",NULL};
char *envp[]={0,NULL};
error=execve("test1",argv2,envp);
if(0 != error){
printf("error=%d
",error);
}
parentPid = getpid();
listCaps(parentPid);
cap_free(caps);
return 0;
}
test1.c //被调用者
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#undef _POSIX_SOURCE
#include <sys/capability.h>
extern int errno;
void listCaps(pid_t pid)
{
uid_t ruid;
uid_t euid;
uid_t suid;
getresuid(&ruid, &euid, &suid);
printf("real_user_id=%d, effictive_user_id=%d, saved_user_id=%d
",ruid,euid,suid);
struct __user_cap_header_struct head={_LINUX_CAPABILITY_VERSION_1, pid};
struct __user_cap_data_struct data={};
capget(&head, &data);
printf("Cap data ES=0x%x, PS=0x%x, IS=0x%x
", data.effective,data.permitted, data.inheritable);
}
int main(int argc, char **argv)
{
int stat;
pid_t parentPid = getpid();
listCaps(parentPid);
return 0;
}
Docker中应用capabilities
上述代码编译方式如下:
首先下载libcap源码
wget https://git.kernel.org/pub/scm/libs/libcap/libcap.git/snapshot/libcap-2.26.tar.gz
为验证方便,将上述文件放在在libcap-2.26/libcap下编译
在libcap.h中引用了一个名为cap_names.h的头文件,但libcap目录下不存在,可以直接从这里拷贝
使用docker namespace中user namespace中的方式创建一个unprivileged类型的docker容器(有独立的user namespace)
docker run -itd centos:latest /bin/sh
在host上找到对应该进程的PID,可以在/proc/$PID/task/TID/status中查看进程的capabilities信息,截图如下
CapInh: 00000000a80625fb
CapPrm: 00000000a80625fb
CapEff: 00000000a80625fb
CapBnd: 00000000a80625fb
可以使用capsh来解析上述值
# capsh --decode=00000000a80625fb
0x00000000a80625fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_rawio,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
当前也可以通过容器启动进程/bin/sh映射到host上的pid查看对应的capabilities
# getpcaps 5318
Capabilities for `5318': = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_rawio,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip
将编译好的test和test1拷贝到创建的容器中,当前用户ID如下
sh-4.2# id
uid=0(root) gid=0(root) groups=0(root)
执行./test可以看到如下信息,当前进程(执行execev之前)的capabilities如下,由于进程此时由于getchar()中断,使用上述方式查看当前进程的信息如下(虽然该进程的所有uid为0,但其并不是一个真正系统级别的UID,其UID经过了user namespace的映射,映射到/etc/subuid)
sh-4.2# ./test
real_user_id=0, effictive_user_id=0, saved_user_id=0
Cap data ES=0x80000103, PS=0x80000103, IS=0x80000103
CapInh: 0000000080000103
CapPrm: 0000000080000103
CapEff: 0000000080000103
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
回车,看到完整的信息如下,虽然test1没有设置文件capabilities,但由于test1的uid都是0,仍然会部分遵守上述提到的"当执行一个set-user-ID-root程序,或程序进程的RUD或EUID为0时,则该文件的inheritable集合和permitted集合设置为全1"的规则(没有全部继承是受bounding集合的限制)
sh-4.2# ./test
real_user_id=0, effictive_user_id=0, saved_user_id=0
Cap data ES=0x80000103, PS=0x80000103, IS=0x80000103
real_user_id=0, effictive_user_id=0, saved_user_id=0
Cap data ES=0xa80425fb, PS=0xa80425fb, IS=0x80000103
- 当执行一个set-user-ID-root程序,或程序进程的RUD或EUID为0时,则该文件的inheritable集合和permitted集合设置为全1
- 当执行一个set-user-ID-root程序,或程序进程的EUID为0时,则该文件的effective bit设置为1
在容器中创建一个新的用户,其用户和组都是captest,并修改test和test1的用户和组
useradd captestchown captest:captest test*chmod 777 test*
切换到captest用户下执行./test会返回”capset(): Operation not permitted“的错误,查看该用户的进程
[captest@78d4c79e2648 home]$ ps -ef
UID PID PPID C STIME TTY TIME CMD
captest 204 203 0 22:37 pts/3 00:00:00 bash
查看上述bash进程的capabilities信息如下,可以看到effective集合被清空了,这样就导致bash下的子进程的effective集合也是空,没有权限执行。
CapInh: 00000000a80425fb
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
bash进程的父进程为“su captest”,其EUID为root,即0,该进程会执行bash命令,即EUID从0 转变为非0。
root 203 186 0 21:53 pts/1 00:00:00 su captest
captest 204 203 0 21:53 pts/1 00:00:00 bash
上述现象符合如下规则
如果程序的RUID,EUID以及SUID的一个或多个为0,当这些UID的值都转变为非0时,permitted, effective, ambient集合都会被清空
当程序的EUID从0转变为非0,则程序的effective集合会被清空
在captest用户下执行./test1,同样也可以看到,由于udi全部非0,ES和PS都会清空,但IS没有变
[captest@78d4c79e2648 home]$ ./test1
real_user_id=1001, effictive_user_id=1001, saved_user_id=1001
Cap data ES=0x0, PS=0x0, IS=0xa80425fb
在内核4.14版本的时候引入了一个Namespaced file capabilities的概念,主要用于解决VFS_CAP_REVISION_2下file capabilities无法在不同user namespace下隔离的问题。在VFS_CAP_REVISION_3版本之前,一个CAP_SETFCAP进程可以在获取file capabilities权限的时候,并不关心该进程所在的user namespace,如果一个进程拥有CAP_SETFCAP的权限,那它可以通过设置并执行多个file capabilities来获取原来没有的capabilities权限,这样就导致进程权限完全不受user namespace的限制且毫无意义(既然自己可以设置capabilities并通过exec获得,那文件自身拥有的capabilities就没用了)。因此在4.14版本新增了一个rootid的变量,rootid是指新创建的命名空间中的UID 0对应初始user namespace的UID的值,即映射到host主机上的user ID的值(/etc/subuid)
struct vfs_ns_cap_data {
__le32 magic_etc;
struct {
__le32 permitted; /* Little endian */
__le32 inheritable; /* Little endian */
} data[VFS_CAP_U32];
__le32 rootid;
};
TIPS
- capabilities只是给了线程执行某项功能的能力,但线程是否能调用另外的程序,需要看当前线程的EUID是否与文件的EUID对应,且能获取该文件的所有capabilities(如果文件设置了capabilities)
- execve与fork不一样,它并不会创建一个子进程,execve执行程序的时候会对capabilities进行重新计算,如果此时的程序uid不为0或文件capabilities为空,则该程序会失去所有的capabilities。
- 不要轻易给一个线程或文件设置CAP_SYS_ADMIN权限,CAP_SYS_ADMIN的权限类似root
- 在容器中给文件设置capabilities时可能会出现“Operation not permitted”的错误,可以查看host的系统日志,centos上一般时SElinux功能没有关闭导致的,执行setenforce 0即可
- capabilities中定义了各个capabilities的值,但要注意这些值代表的是2的幂次方,如“#define CAP_SETPCAP 8”,则CAP_SETPCAP 的值为2^8=256
- docker可以在run的时候使用--cap-add为容器的初始进程添加capabilities,--cap-drop移除capabilities
- 使用CAP_SETFCAP可以为文件设置任意capabilities,使用CAP_SETPCAP则需要按照一定的规定添加(见上文)
- 一个线程的capabilities集合与自身设置和文件capabilities设置相关,同时也受uid的限制。
- 切换系统用户其实也是执行一个进程(sh或bash)
Setcap的详解和使用
Capabilities的主要思想在于分割root用户的特权,即将root的特权分割成不同的能力,每种能力代表一定的特权操作。例如:能力CAP_SYS_MODULE表示用户能够加载(或卸载)内核模块的特权操作,而CAP_SETUID表示用户能够修改进程用户身份的特权操作。在Capbilities中系统将根据进程拥有的能力来进行特权操作的访问控制。
在Capilities中,只有进程和可执行文件才具有能力,每个进程拥有三组能力集,分别称为cap_effective, cap_inheritable, cap_permitted(分别简记为:pE,pI,pP),其中cap_permitted表示进程所拥有的最大能力集;cap_effective表示进程当前可用的能力集,可以看做是cap_permitted的一个子集;而cap_inheitable则表示进程可以传递给其子进程的能力集。系统根据进程的cap_effective能力集进行访问控制,cap_effective为cap_permitted的子集,进程可以通过取消cap_effective中的某些能力来放弃进程的一些特权。可执行文件也拥有三组能力集,对应于进程的三组能力集,分别称为cap_effective, cap_allowed 和 cap_forced(分别简记为fE,fI,fP),其中,cap_allowed表示程序运行时可从原进程的cap_inheritable中集成的能力集,cap_forced表示运行文件时必须拥有才能完成其服务的能力集;而cap_effective则表示文件开始运行时可以使用的能力。
(一)Linux内核中Capabilities的实现机制
Linux内核从2.2版本开始,就加进的Capabilities的概念与机制,并随着版本升高逐步得到改进。在linux中,root权限被分割成一下29中能力:
CAP_CHOWN:修改文件属主的权限
CAP_DAC_OVERRIDE:忽略文件的DAC访问限制
CAP_DAC_READ_SEARCH:忽略文件读及目录搜索的DAC访问限制
CAP_FOWNER:忽略文件属主ID必须和进程用户ID相匹配的限制
CAP_FSETID:允许设置文件的setuid位
CAP_KILL:允许对不属于自己的进程发送信号
CAP_SETGID:允许改变进程的组ID
CAP_SETUID:允许改变进程的用户ID
CAP_SETPCAP:允许向其他进程转移能力以及删除其他进程的能力
CAP_LINUX_IMMUTABLE:允许修改文件的IMMUTABLE和APPEND属性标志
CAP_NET_BIND_SERVICE:允许绑定到小于1024的端口
CAP_NET_BROADCAST:允许网络广播和多播访问
CAP_NET_ADMIN:允许执行网络管理任务
CAP_NET_RAW:允许使用原始套接字
CAP_IPC_LOCK:允许锁定共享内存片段
CAP_IPC_OWNER:忽略IPC所有权检查
CAP_SYS_MODULE:允许插入和删除内核模块
CAP_SYS_RAWIO:允许直接访问/devport,/dev/mem,/dev/kmem及原始块设备
CAP_SYS_CHROOT:允许使用chroot()系统调用
CAP_SYS_PTRACE:允许跟踪任何进程
CAP_SYS_PACCT:允许执行进程的BSD式审计
CAP_SYS_ADMIN:允许执行系统管理任务,如加载或卸载文件系统、设置磁盘配额等
CAP_SYS_BOOT:允许重新启动系统
CAP_SYS_NICE:允许提升优先级及设置其他进程的优先级
CAP_SYS_RESOURCE:忽略资源限制
CAP_SYS_TIME:允许改变系统时钟
CAP_SYS_TTY_CONFIG:允许配置TTY设备
CAP_MKNOD:允许使用mknod()系统调用
CAP_LEASE:允许修改文件锁的FL_LEASE标志
使用例子:
安装 Wireshark 的时候,有一步是给 dumpcap 读网卡的权限,使得普通用户也可以使用 Wireshark 进行抓包。
# setcap 'CAP_NET_RAW+eip CAP_NET_ADMIN+eip' /usr/sbin/dumpcap
使用SetUID设置普通用户针对某个文件可以使用root用户权限
chmod u+s /path/to/application`
在 root 用户下,用 setcap 命令给 /bin/ping 这个可执行文件加上 “cap_net_admin,cap_net_raw+ep” 权限,普通用户即可使用 ping.
#setcap 'cap_net_admin,cap_net_raw+ep' /bin/ping
使用getcap查看命令
查看:[root@rhel671~]
# getcap /bin/ping``/bin/ping = cap_net_admin,cap_net_raw+ep
给普通用户放开开通1024以内端口的权限
setcap cap_net_bind_service=+eip /data/soft/nginx/bin/nginx
-r是清除附加权限:setcap -r nginx
设置TOMCAT可以非ROOT用户启动 1024以下端口
setcap cap_net_bind_service=+eip /opt/tomcat/bin/startup.sh
参考:
http://man7.org/linux/man-pages/man7/capabilities.7.html
https://raesene.github.io/blog/2017/08/27/Linux-capabilities-and-when-to-drop-all/
https://review.lineageos.org/c/LineageOS/android_kernel_samsung_apq8084/+/192902/6
https://github.com/riyazdf/dockercon-workshop/tree/master/capabilities
https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
https://s3hh.wordpress.com/2017/09/20/namespaced-file-capabilities/