如何在Pod中执行宿主机上的命令

基础知识回顾

要回答标题中的疑问,我们首先要清楚,Pod是什么?

Pod的翻译叫容器组,顾名思义,是一组容器。叫做“组”是因为这些容器:

  1. 总是被同时调度,调度到同一节点

  2. 共享网络,具有相同的IP地址和端口空间,可以通过localhost相互访问

  3. 可以基于SystemV信号量、POSIX消息队列等方式,进行进程间通信

  4. 共享存储卷(需要各自分别挂载)

从效果上看,容器组运行在一个虚拟的“主机”中。这个“主机”基于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命名空间都是独立的:

  1. /proc/sys/fs/mqueue POSIX消息队列

  2. /proc/sys/kernel下的System V接口,包括msgmax, msgmnb, msgmni, sem, shmall, shmmax, shmmni,shm_rmid_forced

  3. /proc/sysvipc下的System V接口

要启用IPC命名空间,在构建内核时需要指定 CONFIG_IPC_NS选项。

创建新进程时,使用 CLONE_NEWIPC标记可以启用新的IPC命名空间。Network命名空间

该命名空间隔离:

  1. 网络设备

  2. IPv4/IPv6网络栈

  3. IP路由表

  4. IPtables

  5. 端口(套接字)

  6. /proc/net(/proc/self/net)目录

  7. /sys/class/net目录

  8. /proc/sys/net下若干文件

  9. 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命名空间的初始化挂载点列表如下:

  1. 如果命名空间通过 clone创建,则挂载点列表是父进程Mount命名空间挂载点列表的副本

  2. 如果命名空间通过 unshare创建,则挂载点列表取决于调用者的Mount命名空间

默认情况下,使用mount/umount系统调用修改挂载点列表,不会影响其它命名空间。注意点

关于Mount命名空间,需要注意:

  1. 每个Mount命名空间归属于一个User命名空间。上文提到新创建Mount命名空间会复制挂载点,如果两个Mount命名空间所属的User命名空间不同,则新命名空间是less privileged的

  2. 当前创建了less privileged的Mount命名空间时,Shared Mount退化为Slave Mount,以确保在less privileged空间进行的mapping不会传播到more priviledeged命名空间

  3. 来自more privileged命名空间的挂载点,是一个不可分割的整体,不能在less privileged命名空间中被分离

  4. mount调用的选项MS_RDONLY、MS_NOSUID、MS_NOEXEC,以及MS_NOATIME、MS_NODIRATIME、MS_RELATIME被锁定,不能在less privileged中被修改

  5. 一个文件或目录,在命名空间A中可能是挂载点,在命名空间B则不是挂载点。这些目录或文件可能被重命名、unlink或者删除,其结果是,那些将其作为挂载点的命名空间,对应的挂载点会被删除。在3.18-版本中,重命名、unlink、删除这种挂载点目录,会导致EBUSY错误

共享子树

在某些情况下,Mount命名空间提供的隔离太重了。举例来说,要让一个新载入的光盘能够在所有命名空间可见,必须在每个命名空间执行挂载操作。为了避免这种麻烦,从2.6.15开始,内核引入了共享子树特性,该特性允许跨越命名空间的、受控传播的自动mount/umount。

每个挂载点的传播类型,可以设置为:

  1. MS_SHARED:表示该挂载点在对等组(Peer Group)成员之间共享mount/umount事件。也就是说组中任何命名空间进行了mount,其它命名空间自动的也进行mount

  2. MS_PRIVATE:表示该挂载点是私有的,不具有对等组

  3. MS_SLAVE:允许来自(Master)共享对等组的mount/umount事件传播到此挂载点。反之,该挂载点发起的mount/umount事件则不会传播。注意一个对等组可以是另外一个的Slave

  4. MS_UNBINDABLE:类似于MS_PRIVATE,增加一个额外限制,禁止绑定挂载(mount --bind,用于将某个目录挂载到另一个位置,两个地方内容一致)

新创建的挂载点加入旧挂载点所属的对等组的条件是:

  1. 旧挂载点被设置为MS_SHARED

  2. 并且,满足以下之一:

    1. 创建新命名空间时旧挂载点被复制

    2. 从旧挂载点创建了一个新的绑定挂载

PID命名空间

该命名空间隔离进程ID空间,允许不同命名空间中的进程拥有相同的ID。要启用此特性,编译内核时需要指定 CONFIG_PID_NS选项。

PID命名空间允许容器:

  1. 暂停/恢复容器中的一组进程

  2. 将容器迁移到新的宿主机,而保持容器中进程的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。

一个进程可以被以下进程看到:

  1. 同一命名空间内的进程

  2. 直接或者间接的祖先命名空间中的进程

这意味着初始命名空间可以看到所有进程。反之,子代命名空间则不能看到祖代命名空间中的进程。

所谓“看到”,是指可以以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加入即有命名空间后,进程具有目标命名空间的所有能力。相反的,进程在它的祖代命名空间、或者先前的命名空间,则没有任何能力。

确定命名空间中一个进程是否具有某项能力的规则如下:

  1. 如果进程的有效能力集(Effective capability set)中设置了能力A,则它具有所在命名空间的能力A。进程的有效能力集可以因多种原因设置:

    1. 执行了Set User ID的程序

    2. 执行了具有关联的能力的可执行文件

    3. 因为clone/unshare/setns调用而设置的能力

  2. 如果进程在命名空间中具有能力,则在该命名空间的所有子代命名空间中同样具有相同的能力

  3. 当User命名空间被创建时,内核将创建它的进程的有效UID记录为命名空间的Owner。此命名空间的父命名空间中 ,任何有效UID为Owner的进程,都具有该User命名空间的所有能力

能力的作用

如果进程在User命名空间中具有能力,那么它就可以对该命名空间管理的资源执行相应的特权操作。更精确的说,是对该User命名空间所拥有(关联)的(非User)命名空间所管理的资源具有特权操作。举例来说,进程P1所于UTS命名空间N1,N1属于User命名空间N2,那么,P1必须在N2中持有CAP_SYS_ADMIN能力,才能执行sethostname系统调用。

从另一角度来说,很多特权操作会影响到,不被任何命名空间管理的资源。例如:

  1. 修改时间(对应能力CAP_SYS_TIME)

  2. 加载内核模块(对应能力CAP_SYS_MODULE)

  3. 创建设备(对应能力CAP_MKNOD)

这些操作,仅仅能由位于初始User命名空间中的特权进程执行。

在拥有进程所属Mount命名空间的User命名空间中,持有CAP_SYS_ADMIN能力,允许进程:

  1. 创建Bind挂载

  2. 挂载以下类型的文件系统

    1. /proc,要求内核3.8+

    2. /sys,要求内核3.8+

    3. devpts,要求内核3.9+

    4. tmpfs,要求内核3.9+

    5. ramfs,要求内核3.9+

    6. mqueue,要求内核3.9+

    7. bpf,要求内核4.4+

在拥有进程所属Cgroup命名空间的User命名空间中,持有CAP_SYS_ADMIN能力,允许进程:

  1. 从内核4.6开始,挂载Cgroup v2文件系统

  2. 挂载Cgroup v1命名结构(即通过选项none,name=挂载的Cgroups文件系统)

但是,挂载块设备的文件系统,必须要求进程具有初始User命名空间的CAP_SYS_ADMIN能力。

在拥有进程所属PID命名空间的User命名空间中,持有CAP_SYS_ADMIN能力,允许进程:

  1. 挂载/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

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: centos
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: centos
  template:
    metadata:
      labels:
        name: centos
    spec:
      # 加入宿主机网络命名空间
      hostNetwork: true
      # 加入宿主机PID命名空间
      hostPID: true
      # 加入宿主机IPC命名空间
      hostIPC: true 
      containers:
      - image: docker.gmem.cc/centos:7.6
        imagePullPolicy: Always
        name: centos
        securityContext:
          # 设置PID为root
          runAsUser: 0
          # 特权模式
          privileged: true
        volumeMounts:
        # 这个挂载允许容器中的systemctl和宿主机的systemd通信
        - name: dbus
          mountPath: /var/run/dbus
        - name: run-systemd
          mountPath: /run/systemd
        # 这个挂载允许查看宿主机的systemd配置
        - name: etc-systemd
          mountPath: /etc/systemd
        # 这个挂载允许容器读取非journald管理的日志
        - name: var-log
          mountPath: /var/log
        - name: var-run
          mountPath: /var/run
        - name: run
          mountPath: /run
        - name: usr-lib-systemd
          mountPath: /usr/lib/systemd
      volumes:
      - name: dbus
        hostPath:
          path: /var/run/dbus
          type: Directory
      - name: run-systemd
        hostPath:
          path: /run/systemd
          type: Directory
      - name: etc-systemd
        hostPath:
          path: /etc/systemd
          type: Directory
      - name: var-log
        hostPath:
          path: /var/log
          type: Directory
      - name: var-run
        hostPath:
          path: /var/run
          type: Directory
      # /var/run 是 /run的符号链接
      - name: run
        hostPath:
          path: /run
          type: Directory
      - name: usr-lib-systemd
        hostPath:
          path: /usr/lib/systemd
          type: Directory

切换命名空间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