先来看看背景,起因是来自于最近在写一个 CI/CD 系统,在打包的过程中需要 制作 docker 镜像,也就是会与 docker daemon 交互。
调试的时候没问题,当放到笔者自己写的进程管理工具运行时,就报错了。
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.39/build?buildargs=%7B%7D&cachefrom=%5B%5D&cgroupparent=&cpuperiod=0&cpuquota=0&cpusetcpus=&cpusetmems=&cpushares=0&dockerfile=Dockerfile&labels=%7B%7D&memory=0&memswap=0&networkmode=default&rm=1&session=tpiqs5dvsc1yyh7huvieqs6mx&shmsize=0&t=registry.ops.gaoshou.me%2Fposeidon-demo%3ANone&target=&ulimits=null&version=1: dial unix /var/run/docker.sock: connect: permission denied
time="2019-06-26T17:02:50+08:00" level=error msg="failed to dial gRPC: cannot connect to the Docker daemon. Is 'docker daemon' running on this host?: dial unix /var/run/docker.sock: connect: permission denied"
我们知道与本地
docker daemon 交互一般是通过 /var/run/docker.sock
通信的。典型的权限如下
srw-rw---- 1 root docker 0 Jun 26 14:15 /var/run/docker.sock
既然 UNIX-LIKE 系统把设备都当作文件处理,那权限问题首先想到的就是在进程管理 工具下运行的进程,权限到底是不是正确呢? 于是赶紧再写一个简单的测试来验证
command = id
user = someuser
输出
command: id
uid=999(someuser) gid=65534(nogroup) groups=65534(nogroup),0(root)
而实际上这个用户的身份如下
uid=999(someuser) gid=65534(nogroup) groups=65534(nogroup),27(sudo),100(users),105(crontab),118(docker)
于是怀疑切换进程权限时,是否有问题呢。刚开始怀疑是 pola 中执行子进程
使用 /bin/sh
的锅。 debian/ubuntu 系统下 /bin/sh
指向的是 dash 。而实际上
测试下来与 dash 无关。那么答案应该比较清楚,就是 pola 自己的问题了。
我们再来看一下, pola 切换用户需要使用 root 权限或者 sudo 运行,此时顶级的进程
权限是 root:root
,也即 uid=0, gid=0
。对比原来的代码:
void switch_user(const char * user, char ** env)
{
if (user == NULL || !strcmp("", user))
return;
struct passwd *p;
if ((p = getpwnam(user)) == NULL) {
fprintf(stderr, "cannot find user: %s\n", user);
exit(1);
}
uid_t uid = getuid();
if (uid == p->pw_uid) return;
if (uid != 0) {
fprintf(stderr, "cannot switch user without root");
exit(1);
}
if (setgid(p->pw_gid) == -1) {
fprintf(stderr, "cannot setgid -> %d\n", p->pw_gid);
exit(1);
}
printf("[%s] setgid to %d\n", sys_argv[0], p->pw_gid);
if (setuid(p->pw_uid) == -1) {
fprintf(stderr, "cannot setuid -> %d\n", p->pw_uid);
exit(1);
};
printf("[%s] setuid to %d\n", sys_argv[0], p->pw_uid);
snprintf(env[0], strlen(p->pw_name) + 6, "USER=%s", p->pw_name);
snprintf(env[1], strlen(p->pw_dir) + 6, "HOME=%s", p->pw_dir);
snprintf(env[2], strlen(p->pw_name) + 7, "LOGIN=%s", p->pw_name);
}
switch_user
方法仅仅调用了 setgid
与 setuid
。我们看到最终的 uid 与 gid 是
正确的,但是后面的 groups
不对,这是导致无法访问 /var/run/docker.sock
的直接原因
Linux 系统的用户组
在 Linux 或者说 UNIX-like 的操作系统中,每个用户除了自己的用户 id 以及 首要分组id
以外,还可以加入其他附加的分组,这个概念叫做 Supplementary groups
。
当访问资源时,会鉴别除了主要分组外,再加上这个附加分组的身份,来决定最终是否能访问。
实际上,当某个进程访问资源时,其权限并不一定都是由开启进程时的权限决定。
例如进程管理程序中常用的,使用 root 权限的可以通过 setuid
与 setgid
来
设置 “生效”的uid 与 gid 。
设置进程附加分组权限
那么既然 uid 与 gid 可以设置,附加分组是否可以设置呢?当然可以,参考 setgroups(2)
与 syscalls(2)
。
系统调用函数原型:
int setgroups(size_t size, const gid_t * list);
接下来就要根据输入的用户名,先获取具有哪些附加分组。 glibc 与 musl 都提供了
getgrouplist(3)
。不过实际测试发现使用 musl 静态编译后的 getgrouplist 调用
在 glibc 环境下跑会报 ESPIPE / invalid seek
错误。
那稍微麻烦一点,我们自己获取吧。使用 getgrent(3)
。
int mygetgrouplist(const char *user, gid_t gid, gid_t *groups, int *ngroups)
{
size_t n, i;
struct group *gr;
if (*ngroups<1) return -1;
n = *ngroups;
*groups++ = gid;
*ngroups = 1;
setgrent();
while ((gr = getgrent()) && *ngroups < 0x7fffffff) {
// printf("%d th run...\n", c++);
for (i=0; gr->gr_mem[i] && strcmp(user, gr->gr_mem[i]); i++);
if (!gr->gr_mem[i]) continue;
// printf("gr->gr_gid: %d\n", gr->gr_gid);
if (++*ngroups <= n) *groups++ = gr->gr_gid;
}
endgrent();
return *ngroups > n ? -1 : *ngroups;
}
这个模仿 getgrouplist
,当实际附加分组超过传递进来的上限时返回 -1 ,并设置数量
提供后一次重新获取。所以外部可以调用两次来获取完整的结果。
int ngroups = 5;
gid_t * groups = calloc(ngroups, sizeof(gid_t *));
if (mygetgrouplist(argv[1], gid, groups, &ngroups) == -1) {
// printf("1st getgrouplist failed...\n");
free(groups);
groups = calloc(ngroups, sizeof(gid_t *));
if (mygetgrouplist(argv[1], gid, groups, &ngroups) == -1) {
perror("2nd getgrouplist error");
exit(1);
}
}
printf("got %d groups\n", ngroups);
for (int i = 0;i < ngroups; i++) {
printf("%d\n", groups[i]);
}
修复
最终我们在 switch_user
方法的 setuid 与 setgid 前先调用这一段代码。就可以实现
根据输入的用户名,同时设置对应的 uid / gid / supplementary groups
。
void switch_user(const char * user, char ** env)
{
if (user == NULL || !strcmp("", user))
return;
struct passwd *p;
if ((p = getpwnam(user)) == NULL) {
fprintf(stderr, "cannot find user: %s\n", user);
exit(1);
}
uid_t uid = getuid();
if (uid == p->pw_uid) return;
if (uid != 0) {
fprintf(stderr, "cannot switch user without root");
exit(1);
}
// should fix before setuid
printf("[%s] apply setgroups_fix\n", sys_argv[0]);
// use first
int ngroups = 1;
gid_t * groups = calloc(ngroups, sizeof(gid_t *));
printf("[%s] >>> get group list\n", sys_argv[0]);
// first run, use 1 size, intentionally fetch real ngroups size
// why not vanilla getgrouplist ?
// musl static build will not work with glibc system
// invalid seek -> ESPIPE error
if (mygetgrouplist(p->pw_name, p->pw_gid, groups, &ngroups) == -1) {
printf("[%s] >>> ngroups: %d\n", sys_argv[0], ngroups);
free(groups);
groups = calloc(ngroups, sizeof(gid_t *));
printf("[%s] >>> got %d groups\n", sys_argv[0], ngroups);
if (mygetgrouplist(p->pw_name, p->pw_gid, groups, &ngroups) == -1) {
printf("ngroups: %d\n", ngroups);
perror("getgrouplist error");
exit(1);
}
}
printf("[%s] >>> set group list\n", sys_argv[0]);
if (setgroups(ngroups, groups) == -1) {
perror("setgroups() failed");
exit(1);
}
if (setgid(p->pw_gid) == -1) {
fprintf(stderr, "cannot setgid -> %d\n", p->pw_gid);
exit(1);
}
printf("[%s] setgid to %d\n", sys_argv[0], p->pw_gid);
if (setuid(p->pw_uid) == -1) {
fprintf(stderr, "cannot setuid -> %d\n", p->pw_uid);
exit(1);
};
printf("[%s] setuid to %d\n", sys_argv[0], p->pw_uid);
snprintf(env[0], strlen(p->pw_name) + 6, "USER=%s", p->pw_name);
snprintf(env[1], strlen(p->pw_dir) + 6, "HOME=%s", p->pw_dir);
snprintf(env[2], strlen(p->pw_name) + 7, "LOGIN=%s", p->pw_name);
}
参考
-
http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_140
-
http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_370
-
http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_328
\_\_END\_\_