如何在Pod中执行宿主机上的命令
基础知识回顾
要回答标题中的疑问,我们首先要清楚,Pod是什么?
Pod的翻译叫容器组,顾名思义,是一组容器。叫做“组”是因为这些容器:
总是被同时调度,调度到同一节点
共享网络,具有相同的IP地址和端口空间,可以通过localhost相互访问
可以基于SystemV信号量、POSIX消息队列等方式,进行进程间通信
共享存储卷(需要各自分别挂载)
从效果上看,容器组运行在一个虚拟的“主机”中。这个“主机”基于Linux命名空间、cgroups等机制和宿主机相互隔离。
虽说容器具有隔离性,但是这种隔离程度远远不如虚拟机,容器本质上就是进程。内核也提供了接口,允许你切换命名空间。只需要切换到宿主机的初始(Initial)命名空间,理论上就可以运行宿主机文件系统中的任何程序,并保证程序的行为正常。Linux命名空间
所谓命名空间,是Linux系统资源的隔离机制。进程可以加入到命名空间,并共享其中的系统资源。命名空间是容器技术的基础之一。
对于外面的进程,命名空间中的资源不可见的, 反之,内部的进程感觉自己拥有完整的全局资源。举例来说,PID是一种系统资源,每个命名空间都可以拥有自己的、值为1的PID,而不会出现混乱。Cgroup命名空间
该命名空间虚拟化进程的Cgroups视图,也就是通过 /proc/[pid]/cgroup、 /proc/[pid]/mountinfo看到的Cgroup路径。
每个Cgroup命名空间具有自己的Cgroup根目录集合,文件/proc[pid]/cgrpup中的路径,都是相对于这些根目录。在 clone或 unshare进程时,你可以指定 CLONE_NEWCGROUP标记,这会导致进程当前的cgroups目录变为新命名空间的cgroup根目录。此规则对于cgroups v1 v2均适用。
当你通过/proc[pid]/cgrpup查看目标进程都归属于哪个Cgroup时,该文件的每一行的第3字段相对于当前(读取Cgroup文件的)进程的对应Cgroup子系统根目录。如果目标进程所属Cgroup目录在当前进程Cgroup对应子系统根目录之外,则第3字段中会出现 ../表示上级目录。
上面这段规则很拗口,我们结合例子看: Shell
12345678910111213141516171819202122232425262728
# 在freezer子系统中创建一个子组mkdir -p /sys/fs/cgroup/freezer/sub1 # 创建一个长时间运行的进程sleep 10000 &[1] 20124# 将上述进程加入新创建的子组echo 20124 > /sys/fs/cgroup/freezer/sub1/cgroup.procs # 现在,创建一个新的子组mkdir -p /sys/fs/cgroup/freezer/sub2# 将当前Shell加入到新子组echo $$30655echo 30655 > /sys/fs/cgroup/freezer/sub2/cgroup.procs# 查看当前进程所属freezer组cat /proc/self/cgroup | grep freezer# 输出 相对于当前组Cgroup根目录,也就是 /sys/fs/cgroup/freezer/sub2/7:freezer:/sub2 # 最后,在一个新的,在新的Cgroups中执行Shell:cat /proc/self/cgroup | grep freezer7:freezer:/cat /proc/20124/cgroup | grep freezer7:freezer:/../sub1
IPC命名空间
该命名空间隔离进程间通信资源,也就是System V IPC对象、 POSIX消息队列。以下/proc接口在每个IPC命名空间都是独立的:
/proc/sys/fs/mqueue POSIX消息队列
/proc/sys/kernel下的System V接口,包括msgmax, msgmnb, msgmni, sem, shmall, shmmax, shmmni,shm_rmid_forced
/proc/sysvipc下的System V接口
要启用IPC命名空间,在构建内核时需要指定 CONFIG_IPC_NS选项。
创建新进程时,使用 CLONE_NEWIPC标记可以启用新的IPC命名空间。Network命名空间
该命名空间隔离:
网络设备
IPv4/IPv6网络栈
IP路由表
IPtables
端口(套接字)
/proc/net(/proc/self/net)目录
/sys/class/net目录
/proc/sys/net下若干文件
Unix Domain Socket
等网络相关资源。创建新进程时,使用 CLONE_NEWNET标记可以开启新的网络命名空间。
每个物理网络设备,仅仅能存在于单个网络命名空间中。当网络命名空间终结(命名空间中最后一个进程退出)后,其中的网络设备归还到初始网络命名空间(而不是最后一个进程的父进程所属的网络命名空间)。
一个虚拟网络设备(veth)对,可以用于创建两个网络命名空间之间的、行为类似于管道的隧道,也可以用于创建到其它网络命名空间物理设备的网桥。当网络命名空间终结时,其veth设备自动销毁。
任何网络设备,包括veth都可以在不同网络命名空间中移动。在Kubernetes中基于Calico构建CNI时,对于每个Pod都会创建一个veth对,在Pod网络命名空间为eth0,在宿主机网络命名空间为cali***:Shell
123456789101112131415161718
# 在宿主机上执行ip link list# ...34: cali84f62caf29f@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP mode DEFAULT group default link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 16 # 在容器中执行ip link list# ...4: eth0@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP mode DEFAULT group default link/ether c2:8a:a9:d4:b5:46 brd ff:ff:ff:ff:ff:ff # 容器以宿主机为路由器,和外部通信# 在宿主机执行route -n172.27.68.135 0.0.0.0 255.255.255.255 UH 0 0 0 cali84f62caf29f
要启用网络命名空间,在构建内核时需要指定 CONFIG_NET_NS选项。Mount命名空间
该命名空间隔离挂载点,也就是隔离文件系统目录树中可以看到的内容。
文件 /proc/[pid]/mounts、 /proc/[pid]/mountinfo、 /proc/[pid]/mountstats中的内容,取决于目标进程所属的Mount命名空间。
创建新进程时,使用标记 CLONE_NEWNS可以创建新的Mount命名空间。新Mount命名空间的初始化挂载点列表如下:
如果命名空间通过 clone创建,则挂载点列表是父进程Mount命名空间挂载点列表的副本
如果命名空间通过 unshare创建,则挂载点列表取决于调用者的Mount命名空间
默认情况下,使用mount/umount系统调用修改挂载点列表,不会影响其它命名空间。注意点
关于Mount命名空间,需要注意:
每个Mount命名空间归属于一个User命名空间。上文提到新创建Mount命名空间会复制挂载点,如果两个Mount命名空间所属的User命名空间不同,则新命名空间是less privileged的
当前创建了less privileged的Mount命名空间时,Shared Mount退化为Slave Mount,以确保在less privileged空间进行的mapping不会传播到more priviledeged命名空间
来自more privileged命名空间的挂载点,是一个不可分割的整体,不能在less privileged命名空间中被分离
mount调用的选项MS_RDONLY、MS_NOSUID、MS_NOEXEC,以及MS_NOATIME、MS_NODIRATIME、MS_RELATIME被锁定,不能在less privileged中被修改
一个文件或目录,在命名空间A中可能是挂载点,在命名空间B则不是挂载点。这些目录或文件可能被重命名、unlink或者删除,其结果是,那些将其作为挂载点的命名空间,对应的挂载点会被删除。在3.18-版本中,重命名、unlink、删除这种挂载点目录,会导致EBUSY错误
共享子树
在某些情况下,Mount命名空间提供的隔离太重了。举例来说,要让一个新载入的光盘能够在所有命名空间可见,必须在每个命名空间执行挂载操作。为了避免这种麻烦,从2.6.15开始,内核引入了共享子树特性,该特性允许跨越命名空间的、受控传播的自动mount/umount。
每个挂载点的传播类型,可以设置为:
MS_SHARED:表示该挂载点在对等组(Peer Group)成员之间共享mount/umount事件。也就是说组中任何命名空间进行了mount,其它命名空间自动的也进行mount
MS_PRIVATE:表示该挂载点是私有的,不具有对等组
MS_SLAVE:允许来自(Master)共享对等组的mount/umount事件传播到此挂载点。反之,该挂载点发起的mount/umount事件则不会传播。注意一个对等组可以是另外一个的Slave
MS_UNBINDABLE:类似于MS_PRIVATE,增加一个额外限制,禁止绑定挂载(mount --bind,用于将某个目录挂载到另一个位置,两个地方内容一致)
新创建的挂载点加入旧挂载点所属的对等组的条件是:
旧挂载点被设置为MS_SHARED
并且,满足以下之一:
创建新命名空间时旧挂载点被复制
从旧挂载点创建了一个新的绑定挂载
PID命名空间
该命名空间隔离进程ID空间,允许不同命名空间中的进程拥有相同的ID。要启用此特性,编译内核时需要指定 CONFIG_PID_NS选项。
PID命名空间允许容器:
暂停/恢复容器中的一组进程
将容器迁移到新的宿主机,而保持容器中进程的PID不变
利用PID命名空间,你可以暂停容器中的一组进程,并且在另外一台机器上恢复它们,而保持PID不变。
在新的PID命名空间中,生成的PID从1开始,看起来就像是独立的系统。fork/vfork/clone等系统调用会生成在当前命名空间中唯一的PID。命名空间的Init进程
新命名空间中创建(执行clone/unshare系统调用时指定CLONE_NEWPID标记)的第一个进程,具有PID 1,作为命名空间的"init"进程。此进程将作为命名空间中任何孤儿进程(其父进程提前死亡)的养父进程。
如果PID 1死亡,则内核以SIGKILL信号杀死命名空间中所有其它进程,此行为遵循规则:Init进程是PID命名空间执行正确行为的基础。在此情况下,针对命名空间的后续fork调用会导致ENOMEM错误。
只有已经被Init进程设置了处理器的信号,才可以从命名空间其它进程发送给Init进程。即使是特权进程也不能违反此规则,这防止Init进程被意外的杀死。
类似的,祖先命名空间中的进程,也只有在Init进程设置了处理器的前提下,才能发送信号给Init。但是SIGKILL、SIGSTOP不受限制,这些信号被强制的从祖先命名空间递送给Init进程,并且这两个信号不能被Init进程捕获处理,结果就是导致Init进程终结。
从内核3.4开始,系统调用reboot会导致命名空间中的Init进程接收到信号。嵌套PID命名空间
PID命名空间可以形成树状层次,除了初始(root,initial)命名空间之外,所有PID命名空间都具有一个父命名空间。父命名空间就是调用clone/unshare生成新PID命名空间的那个进程,所属的PID命名空间。从3.7开始,PID命名空间树的深度被限制为最多32。
一个进程可以被以下进程看到:
同一命名空间内的进程
直接或者间接的祖先命名空间中的进程
这意味着初始命名空间可以看到所有进程。反之,子代命名空间则不能看到祖代命名空间中的进程。
所谓“看到”,是指可以以PID为参数,针对目标进程执行系统调用,例如kill、 setpriority。很显然,在每个祖代命名空间看到的,同一个实际进程的PID是不一样的。执行系统调用时,必须使用调用者所在命名空间的PID“视图”。
通过系统调用setns,进程可以自由的加入子代PID命名空间,但是却不能加入祖代命名空间。UTS命名空间
该命名空间提供两种系统标识符 —— 主机名(hostname)、NIS域名——的隔离。
通过clone/unshare系统调用创建新的UTS命名空间时,来自调用者UTS命名空间的标识符会自动拷贝过来。User命名空间
该命名空间隔离安全相关的标识符、属性。包括User IDs、Group IDs、root目录、密钥(keyrings)、能力( Capabilities)。
User命名空间是一个特殊的命名空间,它和其它命名空间具有交互性,它可以拥有(Own)其它命名空间。
一个进程的UID、GID在命名空间内外可以不同。一个进程可以在命名空间内部具有UID 0,而在外部仅仅具有一个非特权UID,这意味着进程可以在一个命名空间内进行特权操作,而在此命名空间之外却不可以。嵌套User命名空间
类似于PID命名空间,User命名空间也是可以嵌套的。除了初始命名空间之外,所有User命名空间都具有一个父命名空间。父命名空间就是创建新命名空间(clone/unshares时指定标记CLONE_NEWUSER)的那个进程所属的User命名空间。
从3.11版本开始,内核限制User命名空间树的最大深度为32。
每个进程都是单个User命名空间的成员,单线程的进程如果具有CAP_SYS_ADMIN能力则可以调用setns来加入其它User命名空间,从而获得目标命名空间的所有能力(capabilities)。
执行系统调用execve会导致能力的重新计算,结果就是,除非进程在当前命名空间具有UID 0或者进程的可执行文件提供了一个非空的可继承的能力掩码(Capabilities mask),进程会丢失所有能力。能力
基于CLONE_NEWUSER创建的进程,拥有新创建的命名空间的所有能力。类似的,通过setns加入即有命名空间后,进程具有目标命名空间的所有能力。相反的,进程在它的祖代命名空间、或者先前的命名空间,则没有任何能力。
确定命名空间中一个进程是否具有某项能力的规则如下:
如果进程的有效能力集(Effective capability set)中设置了能力A,则它具有所在命名空间的能力A。进程的有效能力集可以因多种原因设置:
执行了Set User ID的程序
执行了具有关联的能力的可执行文件
因为clone/unshare/setns调用而设置的能力
如果进程在命名空间中具有能力,则在该命名空间的所有子代命名空间中同样具有相同的能力
当User命名空间被创建时,内核将创建它的进程的有效UID记录为命名空间的Owner。此命名空间的父命名空间中 ,任何有效UID为Owner的进程,都具有该User命名空间的所有能力
能力的作用
如果进程在User命名空间中具有能力,那么它就可以对该命名空间管理的资源执行相应的特权操作。更精确的说,是对该User命名空间所拥有(关联)的(非User)命名空间所管理的资源具有特权操作。举例来说,进程P1所于UTS命名空间N1,N1属于User命名空间N2,那么,P1必须在N2中持有CAP_SYS_ADMIN能力,才能执行sethostname系统调用。
从另一角度来说,很多特权操作会影响到,不被任何命名空间管理的资源。例如:
修改时间(对应能力CAP_SYS_TIME)
加载内核模块(对应能力CAP_SYS_MODULE)
创建设备(对应能力CAP_MKNOD)
这些操作,仅仅能由位于初始User命名空间中的特权进程执行。
在拥有进程所属Mount命名空间的User命名空间中,持有CAP_SYS_ADMIN能力,允许进程:
创建Bind挂载
挂载以下类型的文件系统
/proc,要求内核3.8+
/sys,要求内核3.8+
devpts,要求内核3.9+
tmpfs,要求内核3.9+
ramfs,要求内核3.9+
mqueue,要求内核3.9+
bpf,要求内核4.4+
在拥有进程所属Cgroup命名空间的User命名空间中,持有CAP_SYS_ADMIN能力,允许进程:
从内核4.6开始,挂载Cgroup v2文件系统
挂载Cgroup v1命名结构(即通过选项none,name=挂载的Cgroups文件系统)
但是,挂载块设备的文件系统,必须要求进程具有初始User命名空间的CAP_SYS_ADMIN能力。
在拥有进程所属PID命名空间的User命名空间中,持有CAP_SYS_ADMIN能力,允许进程:
挂载/proc文件系统
和其它命名空间的交互
从3.8开始,非特权进程可以创建User命名空间,而其它类型的命名空间,只需要调用者进程具有所在User命名空间中的CAP_SYS_ADMIN能力。
当一个非User命名空间被创建时,它被创建者进程,在创建它的那个时刻,所属于的User命名空间,所拥有(Own)。
非User命名空间管理了某些系统资源,要对这些资源进行会特权操作,则操作者进程必须在拥有这些命名空间的User命名空间中,持有必要的能力。
执行clone/unshare系统调用时,如果指定了CLONE_NEWUSER的同时,也指定了其它CLONE_NEW*标记,那么内核会确保User命名空间首先被创建。并且将随后创建的其它类型的命名空间的特权赋予子进程(clone)、调用者进程(unshare)。
执行clone/unshare系统调用时,如果指定了非CLONE_NEWUSER之外的CLONE_NEW*标记,则内核会将调用者进程的User命名空间作为新创建的命名空间的Owner。这种Owner关系是可以被改变的。UID/GID映射
对于新创建的User命名空间,它的UID/GID到父User命名空间对应的UID/GID的映射关系,是空的。
UID/GID映射关系,通过文件 /proc/[pid]/uid_map 和 /proc/[pid]/gid_map暴露。这些文件可以在一个User命名空间中读取,可以被写入单次以定义映射关系。
注意,uid_map文件中定义的是,以读取者进程的视角来看,PID进程所属的User命名空间的UID ⇨ 读取者进程所属的User命名空间的UID的映射关系。对于gid_map类似。相关命令
参考Linux命令知识集锦。相关系统调用setns
该系统调用用于将一个线程关联到命名空间:C
12345678910111213141516171819
#define _GNU_SOURCE#include <sched.h> // fd:引用目标命名空间的文件描述符,对应/proc/[pid]/ns/目录中的一个条目// nstype:执行此系统调用的线程可以关联那些类型的命名空间:// 0 允许任何类型的命名空间// CLONE_NEWIPC fd必须是IPC命名空间// CLONE_NEWNET fd必须是网络命名空间// CLONE_NEWUTS fd必须是UTS命名空间// CLONE_NEWNS fd必须是Mount命名空间// CLONE_NEWPID fd必须是PID命名空间// CLONE_NEWUSER fd必须是User命名空间// CLONE_NEWCGROUP fd必须是Cgroup命名空间(4.6+)// 返回值:0表示成功,-1表示错误,errno:// EBADF 无效文件描述符// EINVAL fd引用的命名空间类型和nstype不匹配,或者关联线程到命名空间时出现错误// ENOMEM 没有足够内存// EPERM 调用者线程缺少必要的权限(CAP_SYS_ADMIN)int setns(int fd, int nstype);
下面是一个例子:C
1234567891011121314151617
#define _GNU_SOURCE#include <fcntl.h>#include <sched.h>#include <unistd.h>#include <stdlib.h>#include <stdio.h> int main(int argc, char *argv[]){ int fd = open("/proc/1/ns/mnt", O_RDONLY); /* 读取文件描述符 */ if (fd == -1) exit(EXIT_FAILURE); // 加入命名空间 if (setns(fd, 0) == -1) exit(EXIT_FAILURE); // 在命名空间中执行命令 char * argv[] = {"ls", "-l", "/proc", 0}; execvp( "ls", argv);}
注意各种加入各种命名空间,都具有很多限制条件:
命名空间
说明
User
当前进程必须具有目标User命名空间的CAP_SYS_ADMIN权限,这意味着只能进入子代User命名空间
加入命名空间后,将获得目标命名空间的所有能力,不论进程的UID/PID
多线程上下文下(也就是多线程进程)不能通过setns修改User命名空间
进程也不能重新进入自己本来的User命名空间,这个限制防止已经丢弃某种能力的进程通过setns重新获得该能力
出于安全的考虑,如果进程和其它进程共享了文件系统有关的属性,则不能加入到新的mount命名空间
Mount
当前进程必须具有在其本身的User命名空间中具有CAP_SYS_CHROOT、CAP_SYS_ADMIN能力。同时,在拥有目标Mount命名空间的User命名空间中具有CAP_SYS_ADMIN能力
出于安全的考虑,如果进程和其它进程共享了文件系统有关的属性,则不能加入到新的mount命名空间
PID
当前进程在本身的User命名空间,拥有目标PID命名空间的User命名空间中都需要CAP_SYS_ADMIN能力
只有调用者创建的子进程的PID命名空间才被修改,调用者本身的不会修改,这是setns针对PID命名空间的特殊之处
只能切换到子代PID命名空间中
Cgroup
当前进程在本身的User命名空间,拥有目标PID命名空间的User命名空间中都需要CAP_SYS_ADMIN能力
此操作不会改变调用者的Cgroups成员关系 —— 也就是说不会将其移动到其它Cgourp
Network
当前进程在本身的User命名空间,拥有目标PID命名空间的User命名空间中都需要CAP_SYS_ADMIN能力
IPC
UTS
K8S共享命名空间相关Pod字段
在K8S的Pod的Spec中,和命名空间有关的编排配置包括:Go
1234567891011
type PodSpec struct { // 使用宿主机的网络命名空间 HostNetwork bool `json:"hostNetwork,omitempty" protobuf:"varint,11,opt,name=hostNetwork"` // 使用宿主机的PID命名空间 HostPID bool `json:"hostPID,omitempty" protobuf:"varint,12,opt,name=hostPID"` // 使用宿主机的IPC命名空间 HostIPC bool `json:"hostIPC,omitempty" protobuf:"varint,13,opt,name=hostIPC"` // 让所有容器共享同一个PID命名空间,此选项不能和HostPID同时设置 // 启用该选项后,容器的第一个进程不会赋予PID 1 ShareProcessNamespace *bool `json:"shareProcessNamespace,omitempty" protobuf:"varint,27,opt,name=shareProcessNamespace"`}
创建一个启用上述配置,和宿主机共享网络、PID、IPC命名空间的Pod后,通过 kubectl exec执行 ps aux,你会看到宿主机上的进程。
其它User、Mount、Cgroup等几种命名空间,没有相应的配置字段。需要强调的是,Mount命名空间无法共享,容器需要独立的文件系统树来挂载镜像。仅仅通过K8S提供的编排配置,无法实现我们的目标 —— 因为看不到和宿主机一样的文件系统树。K8S安全配置特权模式
上文提到过,内核允许切换到某个命名空间,然后执行应用程序。系统调用setns、unshare、clone等提供了切换命名空间的接口,命令行工具nsenter、unshare也可以实现相同的功能。
如果我们在启用上述配置的容器中执行 nsenter,尝试切换到初始Mount命名空间,会提示Permission denied错误:Shell
12
nsenter -m -t 1 nsenter: cannot open /proc/1/ns/mnt: Permission denied
这说明容器没有足够的权限进行操作。
Kubernets允许容器以特权模式运行,你只需要配置安全上下文即可。安全上下文包括Pod、Container两个级别。Pod安全上下文Go
1234567891011121314151617181920212223242526272829303132
type PodSpec struct { // 提供Pod级别的安全属性,并为容器安全属性提供默认值 SecurityContext *PodSecurityContext `json:"securityContext,omitempty" protobuf:"bytes,14,opt,name=securityContext"`} type PodSecurityContext struct { // 应用到所有容器的SELinux上下文,如果不指定,则容器运行时为每个容器指定随机的SELinux上下文 SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" protobuf:"bytes,1,opt,name=seLinuxOptions"` // 运行容器进程入口点使用的UID,默认从镜像元数据中获取UID RunAsUser *int64 `json:"runAsUser,omitempty" protobuf:"varint,2,opt,name=runAsUser"` // 运行容器进程入口点使用的GID,默认从使用容器运行时的默认值 RunAsGroup *int64 `json:"runAsGroup,omitempty" protobuf:"varint,6,opt,name=runAsGroup"` // 提示容器必须以非Root身份运行,如果设置为true,则Kubelet会在运行时校验镜像 // 确保它不以UID 0运行,如果发现镜像以UID 0 进行则导致启动失败 RunAsNonRoot *bool `json:"runAsNonRoot,omitempty" protobuf:"varint,3,opt,name=runAsNonRoot"` // 额外的补充组,赋予容器的第一个进程,作为组GID的补充 SupplementalGroups []int64 `json:"supplementalGroups,omitempty" protobuf:"varint,4,rep,name=supplementalGroups"` // 一个特殊的、应用到所有容器的补充组 // 某些类型的卷,允许Kubelet修改卷的所有者,这可以确保容器有权访问卷的内容 // 该选项导致: // 1. 卷的所有者GID设置为FSGroup // 2. setgid位被启用,这导致卷中新创建的文件的所有者为FSGroup // 3. 卷中文件的模式和rw-rw----进行或操作,也就是启用所有者、所在组的读写权限 // 如果不配置,kubelet不会修改任何卷的所有者和文件模式 FSGroup *int64 `json:"fsGroup,omitempty" protobuf:"varint,5,opt,name=fsGroup"` // 指定一系列命名空间化的Sysctl键值 // 如果容器运行时不支持某个Sysctl则可能导致启动失败 Sysctls []Sysctl `json:"sysctls,omitempty" protobuf:"bytes,7,rep,name=sysctls"`}
容器安全上下文
容器安全上下文中,有一部分字段和Pod安全上下文一样,它们会覆盖Pod安全上下文中的对应设置。 Go
1234567891011121314151617181920212223242526
type Container struct { SecurityContext *SecurityContext `json:"securityContext,omitempty" protobuf:"bytes,15,opt,name=securityContext"`} type SecurityContext struct { // 需要给容器添加/删除的能力列表,默认能力取决于容器运行时 Capabilities *Capabilities `json:"capabilities,omitempty" protobuf:"bytes,1,opt,name=capabilities"` // 以特权模式运行容器。这种模式下,容器中进程的身份等价于宿主机的root Privileged *bool `json:"privileged,omitempty" protobuf:"varint,2,opt,name=privileged"` // 覆盖Pod上下文设置 SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" protobuf:"bytes,3,opt,name=seLinuxOptions"` // 覆盖Pod上下文设置 RunAsUser *int64 `json:"runAsUser,omitempty" protobuf:"varint,4,opt,name=runAsUser"` // 覆盖Pod上下文设置 RunAsGroup *int64 `json:"runAsGroup,omitempty" protobuf:"varint,8,opt,name=runAsGroup"` // 覆盖Pod上下文设置 RunAsNonRoot *bool `json:"runAsNonRoot,omitempty" protobuf:"varint,5,opt,name=runAsNonRoot"` // 容器的根文件系统是否设置为只读 ReadOnlyRootFilesystem *bool `json:"readOnlyRootFilesystem,omitempty" protobuf:"varint,6,opt,name=readOnlyRootFilesystem"` // 是否允许子进程获得比父进程更多的特权,控制容器进程的no_new_privs标记是否被设置 // 如果容器是运行在特权模式,或者具有CAP_SYS_ADMIN能力,则该配置自动为true AllowPrivilegeEscalation *bool `json:"allowPrivilegeEscalation,omitempty" protobuf:"varint,7,opt,name=allowPrivilegeEscalation"` // 指定该容器的proc挂载类型,默认的 ProcMount *ProcMountType `json:"procMount,omitempty" protobuf:"bytes,9,opt,name=procMount"`}
Privileged
要满足我们的需求,只需要设置容器安全上下文的Privileged为True就足够了。这样你就可以通过nsenter进入宿主机的Mount命名空间,并且随意的运行命令了,例如通过systemctl判断某些服务是否正常运行。K8S完整配置样例
这个样例允许我们在容器中访问宿主机的日志、控制宿主机的systemd,而不需要切换整个Mount命名空间。我们目前项目的一个需求就是,能够读取节点的内核日志环、Journald日志,可以用下面这种卷挂载的方式满足:YAML
切换命名空间nsenter
这是一个命令行工具,能够在指定的命名空间中执行命令。K8S的utils/nsenter包对该命令进行了封装,可以参考。setns
要编程式的切换命名空间,可以利用这个系统调用。
在Go语言下,你需要注意的一点是,setns调用可能需要单线程上下文。而Go运行时是多线程的,你必须在Go运行时启动之前,执行setns调用。要实现这种提前调用,可以利用cgo的constructor技巧,该技巧能够在Go运行时启动之前,执行一个任意的C函数:Go
1234567
/*__attribute__((constructor)) void init() { // 这里的代码会在Go运行时启动前执行 // 它会在单线程的C上下文中运行}*/import "C"
libcontainer提供了基于此技巧的例子。
Last updated