usernamespace在docker中的用法:
- docker 中可以通过dockerd的 –userns-remap=$username 选项来指定用户映射,从文档上来看,该选项只能使用一次,也就是说,只能指定一个用户
- docker 参考的用户映射关系文件为 /etc/subuid 和 /etc/subgid ,文件格式大约为:
1bozo:100000:65536
其中:
bozo: 是–userns-remap 指定的用户名
100000:65536 以为着该用户可以映射到的容器外部的uid的范围为 100000 ~ (100000 + 65536) - 目前来看,不同容器中相同用户映射到容器外面的uid是相同的
- docker文档 要求需要映射的用户必须在容器外面是真实存在的,至少在 /etc/passwd 中是需要存在的; 从uid映射来看,/etc/passwd 中定义的uid基本是被无视的
- 通过–userns-remap伪造的容器内部的root用户,虽然在容器外部就是个普通用户,但是在容器内部确实是有很大的特权的
- docker create的 –user选项和usernamespace没有关系,只是说用哪个用户启动容器内部的进程而已
- 参考: https://github.com/docker/labs/tree/master/security/userns
从上面分析来看,userns-remap使用起来是比较麻烦的、且功能太有限; 我们期望不需要太多配置的情况下,不同容器中所有进程、子进程的用户都能自动映射到外部单独的uid,可能实现这个确实比较麻烦(参考下面原理部分)
username space原理:
- User Namespace是Linux 3.8新增的一种namespace
- clone系统调用CLONE_NEWUSER
- /proc/$pid/uid_map (对于支持user namespace的内核,都存在该文件,不明确设置就为空)
- 当我们通过CLONE_NEWUSER去clone一个进程(或线程)的话,进程的后续行为都会参考 /proc/$pid/uid_map 来决定用户的身份,那么该uid_map 文件必然要在clone之后才能写入了;为了避免我们clone出来的那个进程很快执行到用户代码而实现提权,则往往在clone后执行的是可控的代码,在准备工作完成后在execv()去执行用户的进程(注意: execv并不改变进程号,或者说不产生新的进程,而是在当前进程空间来执行用户的代码),用户代码中很可能会继续clone,那么后续的clone如果没有CLONE_NEWUSER的话,能否参考到上面的/proc/$pid/uid_map 呢?或者说,只参考该pid namespace中init进程下面的uid_map ? (稍后继续研究)
- http://man7.org/linux/man-pages/man7/user_namespaces.7.html
看个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/mount.h> #include <sys/capability.h> #include <stdio.h> #include <sched.h> #include <signal.h> #include <unistd.h> #define STACK_SIZE (1024 * 1024) static char container_stack[STACK_SIZE]; char* const container_args[] = { "/bin/bash", NULL }; int pipefd[2]; void set_map(char* file, int inside_id, int outside_id, int len) { FILE* mapfd = fopen(file, "w"); if (NULL == mapfd) { perror("open file error"); return; } fprintf(mapfd, "%d %d %d", inside_id, outside_id, len); fclose(mapfd); } void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) { char file[256]; sprintf(file, "/proc/%d/uid_map", pid); set_map(file, inside_id, outside_id, len); } void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) { char file[256]; sprintf(file, "/proc/%d/gid_map", pid); set_map(file, inside_id, outside_id, len); } int container_main(void* arg) { printf("Container [%5d] - inside the container!\n", getpid()); printf("Container: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n", (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid()); /* 等待父进程通知后再往下执行(进程间的同步) */ char ch; close(pipefd[1]); read(pipefd[0], &ch, 1); printf("Container [%5d] - setup hostname!\n", getpid()); //set hostname sethostname("container",10); //remount "/proc" to make sure the "top" and "ps" show container's information mount("proc", "/proc", "proc", 0, NULL); execv(container_args[0], container_args); printf("Something's wrong!\n"); return 1; } int main() { const int gid=getgid(), uid=getuid(); printf("Parent: eUID = %ld; eGID = %ld, UID=%ld, GID=%ld\n", (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid()); pipe(pipefd); printf("Parent [%5d] - start a container!\n", getpid()); int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL); printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid); //To map the uid/gid, // we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent //The file format is // ID-inside-ns ID-outside-ns length //if no mapping, // the uid will be taken from /proc/sys/kernel/overflowuid // the gid will be taken from /proc/sys/kernel/overflowgid (container_pid, 0, uid, 1); set_gid_map(container_pid, 0, gid, 1); printf("Parent [%5d] - user/group mapping done!\n", getpid()); /* 通知子进程 */ close(pipefd[1]); waitpid(container_pid, NULL, 0); printf("Parent - container stopped!\n"); return 0; } |
实际问题:
在没有使用–userns-map的情况下,很多容器都使用相同的镜像(所以,相同用户名的uid基本都一样),每个容器都可以通过容器中的sshd进行登录,sshd登录后都会有相应的limits限制,以单个用户最大进程数量为例,一般来讲,root都是没有限制的,其他用户都是有限制的,对于mysql用户,通常会产生很多线程,从容器的角度来看,该用户名下的进程数量确实不多,甚至整个容器都没有几个进程,这时候,启动mysql时却是失败的;原因在于:
- 启动进程这事儿归根结底还是内核干的
- ulimit资源限制最终是从进程属性上生效的,而不是直接从配置文件上生效的
- 容器外部和容器内部看到的uid是相同的
- 其他容器已经在没有ulimit限制(或者配额足够)的情况下启动了很多mysql进程(或线程)了
- 最后,在ulimit限制比较小的情况下启动mysql进程(或线程)自然就会失败的
- 所以,从用户级别的资源限制方面来讲,容器之间的相互影响是比较难以控制的
- docker本来不是让作为虚拟机使用的,所以,类似问题是不会被docker官方在意(和觉察)的,docker中的–userns-map 仅仅考虑了通常情况下单个容器中只有单个(被docker-containerd启动)的进程的场景
- 解决办法: 给用户足够的资源
- 我们可以限制单个容器的进程数量:
- https://segmentfault.com/a/1190000007468509 https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt
- 通过docker create 的 –pids-limit 选项进行配置
- dockerd 启动选项 –default-ulimit 可以设置默认的ulimit
- 目前,我们生产环境使用的docker容器都是通过docker exec 启动进程的,docker exec 进去后,ulimit如下:
12345678910111213141516core file size (blocks, -c) unlimiteddata seg size (kbytes, -d) unlimitedscheduling priority (-e) 0file size (blocks, -f) unlimitedpending signals (-i) 15726max locked memory (kbytes, -l) 64max memory size (kbytes, -m) unlimitedopen files (-n) 65536pipe size (512 bytes, -p) 8POSIX message queues (bytes, -q) 819200real-time priority (-r) 0stack size (kbytes, -s) 8192cpu time (seconds, -t) unlimitedmax user processes (-u) unlimitedvirtual memory (kbytes, -v) unlimitedfile locks (-x) unlimited
问题:
- 如何从容器级别限制进程数量?
参考资料: