如果通过POD的形式来启动多个容器那么它们的名称空间会是共享的么,所以我这里讨论是在默认情况下同一个POD的不同容器的哪些名称空间是打通的。这里先说一下结论,共享的是UTS、IPC、NET、USER。
UTS名称空间
主机名名称空间,保存内核名称、版本以及主机名和域名。默认情况下同一个POD的不同容器是共享UTS的,看下面的配置:
apiVersion: apps/v1
kind: Deployment
metadata:
name: centos-dep
labels:
app: centos
spec:
replicas: 1
selector:
matchLabels:
app: centos
template:
metadata:
labels:
app: centos
spec:
containers:
- name: app1
image: centos
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"]
args:
- sleep 3600
- name: app2
image: centos
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"]
args:
- sleep 3600
运行这个POD,然后分别登陆到不同容器去查看主机名,你会发现主机名一样,而且就是POD的名字,如下图:
另外你通过uname -a
如果查看到的内容是一致的也说明是共享UTS名称空间的。
实验证明,默认情况下同一个POD中的不同容器的UTS名称空间是共享的。
IPC名称空间
进程间通信名称空间,IPC的隔离就是阻断进程间通信,主要是信号量、队列和共享内存。运行主机进程通过上面的机制进行通信。
下面通过一个实验来看看同一个POD的IPC名称空间是否是共享的,在app2中通过命令ipcmk --queue
来创建一个队列,然后在app1中通过命令ipcs
来查看,如果有这个队列就说明是共享的,如下图:
实验证明,默认情况下同一个POD中的不同容器的IPC名称空间是共享的。
MNT名称空间
Mount名称空间,提供对磁盘挂载点和文件系统的隔离能力。同一主机上的不同进程访问相同的路径会得到相同的内容,因为它们共享本地主机的磁盘和文件系统。
在同一POD内容器之间挂载点名称空间是隔离的,如果该POD的多个容器挂载一个POD级别的Volume,那么它们就可以实现挂载点的共享,但共享的也仅仅是这一个Volume并不是整个文件系统。
实验证明,默认情况下同一个POD中的不同容器的MNT名称空间不是共享的。
NET名称空间
网络名称空间,同一主机上的不同进程可以进行localhost或者本地unix socket通信。在单独启动容器的时候不同容器是隔离的,但是在POD中不同容器通过一个Infra容器来进行共享网络名称空间,其原理是其他用户自己定义的容器都Join这个Infra容器的网络。这里我启动的就是一个Cetnos镜像,无法做本地通信验证。不过它的确是通过Infra容器来共享的。
PID名称空间
进程ID名称空间,同一主机上的不同进程在同一PID空间内可以看到其他进程的ID,并且同一PID空间的进程的ID不会重复。另外PID名称空间有层级关系,子空间看不到父空间的内容,但是父空间可以管理子空间,比如发送信号。
在POD中则对应为同一POD内的不同容器可以看到对方的进程ID。默认不是共享的,可以设置POD的shareProcessNamespace
这个值为true来进行共享,默认为false。我在App2中启动一个top命令,然后在App1中通过ps命令查看,看下面的测试:
实验证明,默认情况下同一个POD中的不同容器的PID名称空间不是共享的。
USER名称空间
隔离用户、组以及相关用户能力的。也就是在不同的User Namespace中,相同的用户可以有不同的UID或者不同的权限。另外还可以通过映射的方式把某个User Namespace的用户映射到另外一个User Namespace的用户上,这样这两个名称可能不同的用户就具有相同的权限。如果想要在本机进行验证需要查看一下这个文件:
cat /proc/sys/user/max_user_namespaces
如果是0则表示没有开启,需要给它一个值echo "15000" > /proc/sys/user/max_user_namespaces
,然后你再运行unshare -U或者unshare --user
就不会报错了。
在Docker中默认并没有开启user namespace。
可以看到当前Bash进程和Dockerd进程的名称空间都一样,因为它们都是在同一个名称空间上运行的。另外需要说明的是uip_map的输出,第一个数字是在当前名称里的用户ID,第二个数字是该用户ID在当前名称空间外部被映射到哪个用户ID上,最后一个数字是映射范围。
然后我们启动一个包含两个容器的POD来看一下,如下图:
容器的User namespace和容器外的是一样的,也就是说没有单独为容器创建User namespace,而且容器内的用户ID是0,映射到容器外也是0,这就是意味着容器内的root用户和容器外的root用户拥有相同的权限。说白了就是容器中的进程是以root用户权限运行的,并且这个容器中的root用户和宿主机上的root用户是同一个,看下图,这2个容器进程就是以root运行的:
如果你需要验证,那么你把宿主机上的一个只能由root打开的文件挂载容器中,你看看能不能打开就知道了。
就算你进入容器查看这个sleep 3600其实也是root运行的,简单来说容器内UID为0的root用户就是容器外UID为0的root用户。为什么会是这样呢?在整个系统共享一个内核,而内核只管理一套uid和gid,并且对内核来说只识别uid不识别用户名,也就是说内核在做权限方面它通过uid来做,用户名只是对于用户来讲方便辨认。
不要误认为你在容器中创建一个用户,然后在宿主机也可以看到,因为/etc/password这个文件在不同的文件系统上,容器和宿主机的文件系统还是隔离的。
但有些时候也不要被用户名所迷惑,你应该检查UID,查看容器进程的uid_map中的信息。
让容器进程使用root账号显然不安全,因为它的root就是宿主机的root,所以通常我们会给dockerd进程建立单独的账号或者使用User Namespace。不过推荐使用User Namesapce,因为有些使用容器进程必须以root来运行,如果使用User Namespace的话,我们就可以把宿主机的一个普通用户映射到容器中的root用户,这样容器进程以为自己是root并且在它所在的名称空间内有各种权限,但是在宿主机上它还是普通用户。
如何开启User Namespace呢:
cat /boot/config-3.10.0-957.el7.x86_64 | grep _NS
,先检查一下你的内核是否开启了User Namespace
检查一下是否有下面的文件,如果没有就手动建立:
你可以使用系统中有的用户然后添加到这里,最后在docker的启动参数中加入这个账号,也可以让dockerd自己来建立,如果让dockerd自己来完成,在dockerd的启动docker-daemon.json中加入下面的内容,default表示使用dockerd去建立账号,它使用的名字为dockermap,如果你使用自己的就替换dufault:
{
"userns-remap": "default",
}
在RHEL 7.5版本,上面的配置在dockerd启动的时候会报错"Can't create ID mappings: %!v(MISSING): No subuid ranges found for user "dockremap"",查询之后判断应该是系统BUG,可以看看Redhat官网的Bug说明Bug-1546870,它会在系统中建立dockremap账号然后使用usermod -v参数来设置dockermap用户的ID范围,但是在Centos 7.5版本上的usermod命里没有-v参数。这就意味着RHEL 7.5不支持动态添加subid。所以我们只能手动来做,不过据说其他发行版可以支持比如Ubantu或者Fedora。
向从属用户和组文件中添加范围(如果你使用dockremap账号,那么你无须手动建立,因为dockerd启动的时候就会建立,如果上面的配置是default):
echo "dockremap:10000:65536" > /etc/subuid
echo "dockremap:10000:65536" > /etc/subgid
一共三个字段:
-
第一个字段dockremap,这个一个宿主机上的用户名
-
第二个字段10000,表示子User Namespace中用户ID从哪里开始
-
第三个字段65536,表示子User Namespace中可以有多少个用户ID
整体含义是宿主机的dockremap账号一共有65536个从属用户,用户ID从10000-165535。这个从事用户的ID不是真实的,只是用来分配,它会从这个范围里拿一个ID映射到容器进程里的用户,比如容器进程还是用root用户,其UID实0,那么我们就可以从dockremap这个从属ID中拿一个来映射容器进程中的root。这样容器中看起来是root且具有root权限,但是在宿主机上它就是一个普通账号dockremap的权限。配置好后重启dockerd进程。配置好重新启动POD,如下:
同一个POD中的User Namespace是共享的,但此时它与宿主机的进程就已经不共享User Namespace了。再看一下uid_map
容器中的UID0映射到容器外的从属ID 10000。
不过这样虽然安全但是有些容器进程无论在容器内还是在容器外都需要root账号,比如prometheus的node_explorer,它是以DaemonSet形式运行的需要共享宿主机的网络名称空间,如果以上的用户来运行则会启动失败,如下图:
其实这个和DaemonSet没关系,主要是在docker上启用User Namspace后会有一些限制,userns-remap ,也就是说启用了User Namespace后容器将不能共享宿主机的PID和NET名称空间。所以我想因为有一些限制所以docker默认才不开启User Namespace。不过如果直接通过docker来启动容器可以指定--usens=host
来为某个容器禁用User Namespace,不过在Kubernetes中目前没找到配置POD那个参数可以起到这个效果,有人知道请留言。
实验证明:在默认情况下同一个POD是共享User Namespace的。
最简单的办法来验证一下
在宿主机上找到该POD中的2个容器的容器ID,
通过docker inspect CONTINER_ID --format {{.State.Pid}}
查看两个容器在宿主机上的进程号
通过进程ID查看每个进程的ns情况,左侧红色的是被查看进程名称空间文件,右侧则是该文件指向的具体的Namespace文件,中括号里面的是具体Namespace文件号,如果两个进程的指向的Namespace文件号相同,则说明它们处在同一名称空间。
红色箭头编号相同的就是当前POD中2个容器所共享的名称空间。不过在这里我也有些不明白,uts是共享的可是上图中看到的编号确不一样。因为在宿主机的当前终端运行unshare --uts /bin/bash
命令将会在一个新的uts名称空间打开一个bash程序,这个bash进程和之前那个就是在不同的uts中,看下图:
进行namespace的api操作
对Namespace的API操作包括clone()、setns()和unshare()。它们有一些不同:
-
clone():创建新进场的同时可以创建namespace,通过在这个函数中加入不同的名称空间标志来完成。
-
setns():它是加入一个已经存在的namespace,需要给它传递具体的namespace文件描述符。通常是在调用该函数之后调用clone(),其目的就是让一个新进程在一个已经存在的namespace中运行。
docker exec
就是利用这种机制让你指定的命令在容器中运行。 -
unshare():对当前的进程进行namespace隔离,换句话说它不启动新进程,而是让当前进程或者调用它的进程进入到一个新的namespace中。系统命令unshare就是利用这个调用来实现的。
注意在使用unshare系统调用或者命令或者setns系统调用的时候当涉及到PID Namespace的时候它的处理有些特殊,并不是让调用者进入新的PID Namespace,而是让子进程进入,成为该PID Namespace的1号进程。为什么为这样呢?因为一个进程的PID在系统中是常量,一但一个进程运行它的PID就确定了从而它的父子进程也会被确定,所以不能让它在调用setns或者unshare的时候发生变化,一但变化系统就无法维护这个进程表。