Kubernetes进阶实战(第2版)
上QQ阅读APP看书,第一时间看更新

4.4 容器安全上下文

我们知道,尽管容器技术提供了强大的软件级别的资源隔离功能,但共享内核机制导致其遭受到内部攻击,击破这种隔离的可能性还是要远大于由Hypervisor隔离的虚拟机,更不用说物理级的隔离。容器运行时通常为管理员提供了许多与安全相关的可配置选项,例如可使用的系统调用集与是否可运行为特权模式(具有访问主机设备的权限)等,管理员需要根据这些选项与容器的实际运行需求来精心组织和设定容器的运行时选项以降低安全风险。

Kubernetes为安全运行Pod及容器运行设计了安全上下文机制,该机制允许用户和管理员定义Pod或容器的特权与访问控制,以配置容器与主机以及主机之上的其他容器间的隔离级别。安全上下文就是一组用来决定容器是如何创建和运行的约束条件,这些条件代表创建和运行容器时使用的运行时参数。需要提升容器权限时,用户通常只应授予容器执行其工作所需的访问权限,以“最小权限法则”来抑制容器对基础架构及其他容器产生的负面影响。

Kubernetes支持用户在Pod及容器级别配置安全上下文,并允许管理员通过Pod安全策略在集群全局级别限制用户在创建和运行Pod时可设定的安全上下文。本节仅描述Pod和容器级别的配置,Pod安全策略的话题将在第9章展开。

Pod和容器的安全上下文设置包括以下几个方面。

▪自主访问控制(DAC):传统UNIX的访问控制机制,它允许对象(OS级别,例如文件等)的所有者基于UID和GID设定对象的访问权限。

▪Linux功能:Linux为突破系统上传统的两级用户(root和普通用户)授权模型,而将内核管理权限打散成多个不同维度或级别的权限子集,每个子集称为一种“功能”或“能力”,例如CAP_NET_ADMIN、CAP_SYS_TIME、CAP_SYS_PTRACE和CAP_SYS_ADMIN等,从而允许进程仅具有一部分内核管理功能就能完成必要的管理任务。

▪seccomp:全称为secure computing mode,是Linux内核的安全模型,用于为默认可发起的任何系统调用进程施加控制机制,人为地禁止它能够发起的系统调用,有效降低了程序被劫持时的危害级别。

▪AppArmor:全称为Application Armor,意为“应用盔甲”,是Linux内核的一个安全模块,通过加载到内核的配置文件来定义对程序的约束与控制。

▪SELinux:全称为Security-Enhanced Linux,意为安全加强的Linux,是Linux内核的一个安全模块,提供了包括强制访问控制在内的访问控制安全策略机制。

▪Privileged模式:即特权模式容器,该模式下容器中的root用户拥有所有的内核功能,即具有真正的管理员权限,它能看到主机上的所有设备,能够挂载文件系统,甚至可以在容器中运行容器;容器默认运行于非特权(unprivileged)模式。

▪AllowPrivilegeEscalation:控制是否允许特权升级,即进程是否能够获取比父进程更多的特权;运行于特权模式或具有CAP_SYS_ADMIN能力的容器默认允许特权升级。

这些安全上下文相关的特性多数嵌套定义在Pod或容器的securityContext字段中,而且有些特性对应的嵌套字段还不止一个。而seccomp和AppArmor的安全上下文则需要以资源注解的方式进行定义,而且仅能由管理员在集群级别进行Pod安全策略配置。

4.4.1 配置格式速览

安全上下文可分别设置Pod级别和容器级别,前者的配置将应用到其内部的所有容器之上,而后者的配置则仅在当前容器生效。但有些参数并不适合通用设定,例如特权模式、特权升级、只读根文件系统和内核能力等,它们只可用于容器之上。但也有参数仅可用于Pod级别进行通用设定,例如设置内核参数的sysctl和设置存储卷新件文件默认属组的fsgroup等。下面以Pod资源的配置格式给出了这些配置选项,以便于读者快速预览和了解安全上下文的用法。


apiVersion: v1
kind: Pod
metadata: {…}
spec:
  securityContext:         # Pod级别的安全上下文,对内部所有容器均有效
    runAsUser <integer>    # 以指定的用户身份运行容器进程,默认由镜像中的USER指定
    runAsGroup <integer>   # 以指定的用户组运行容器进程,默认使用的组随容器运行时设定
    supplementalGroups  <[]integer>  # 为容器中1号进程的用户添加的附加组
    fsGroup <integer>      # 为容器中的1号进程附加一个专用组,其功能类似于sgid
    runAsNonRoot <boolean> # 是否以非root身份运行
    seLinuxOptions <Object>  # SELinux的相关配置
    sysctls  <[]Object>         # 应用到当前Pod名称空间级别的sysctl参数设置列表
    windowsOptions <Object>     # Windows容器专用的设置
  containers:
  - name: …
    image: …
    securityContext:            # 容器级别的安全上下文,仅在当前容器生效
      runAsUser <integer>       # 以指定的用户身份运行容器进程
      runAsGroup <integer>      # 以指定的用户组运行容器进程
      runAsNonRoot <boolean>    # 是否以非root身份运行
      allowPrivilegeEscalation <boolean> # 是否允许特权升级
      capabilities <Object>     # 为当前容器添加(add)或删除(drop)内核能力
        add  <[]string>         # 添加由列表定义的各内核能力
        drop  <[]string>        # 移除由列表定义的各内核能力
      privileged <boolean>      # 是否运行为特权容器
      procMount <string>        # 设置容器的procMount类型,默认为DefaultProcMount;
      readOnlyRootFilesystem <boolean> # 是否将根文件系统设置为只读模式
      seLinuxOptions <Object>   # SELinux的相关配置
      windowsOptions <Object>   # Windows容器专用的设置

Kubernetes默认以非特权模式创建并运行容器,同时禁用了其他与管理功能相关的内核能力,但未额外设定其他上下文参数。

4.4.2 管理容器进程的运行身份

制作Docker镜像时,Dockerfile支持以USER指令明确指定运行应用进程时的用户身份。对于未通过USER指令显式定义运行身份的镜像,创建和启动容器时,其进程的默认用户身份为容器中的root用户和root组,该用户有着其他一些附加的系统用户组,例如sys、daemon、wheel和bin等。然而,有些应用程序的进程需要以特定的专用用户身份运行,或者以指定的用户身份运行时才能获得更好的安全特性,这种需求可以在Pod或容器级别的安全上下文中使用runAsUser得以解决,必要时可同时使用runAsGroup设置进程的组身份。

下面的资源清单(securitycontext-runasuer-demo.yaml)配置以1001这个UID和GID的身份来运行容器中的demoapp应用,考虑到非特权用户默认无法使用1024以下的端口号,文件中通过环境变量改变了应用监听的端口。


apiVersion: v1
kind: Pod
metadata:
  name: securitycontext-runasuser-demo
  namespace: default
spec:
  containers:
  - name: demo
    image: ikubernetes/demoapp:v1.0
    imagePullPolicy: IfNotPresent
    env:
    - name: PORT
      value: "8080"
    securityContext:
      runAsUser: 1001
      runAsGroup: 1001

下面的命令先将配置清单中定义的Pod对象securitycontext-runasuser-demo创建到集群上,随后的两条命令验证了容器用户身份确为配置中预设的UID和GID。


~$ kubectl apply -f securitycontext-runasuser-demo.yaml 
pod/securitycontext-runasuser-demo created
~$ kubectl exec securitycontext-runasuser-demo -- id 
uid=1001 gid=1001
$ kubectl exec securitycontext-runasuser-demo -- ps aux
PID    USER      TIME  COMMAND
  1    1001      0:00  python3 /usr/local/bin/demo.py

若有必要,我们还可在上面的配置清单中的安全上下文定义中,同时使用supplement-Groups选项定义主进程用户的其他附加用户组,这对于有着复杂权限模型的应用是一个非常有用的选项。

另外,若运行容器时使用的镜像文件中已经使用USER指令指定了非root用户的运行身份,我们也可以在安全上下文中使用runAsNonRoot参数定义容器必须使用指定的非root用户身份运行,而无须使用runAsUser参数额外指定用户。

4.4.3 管理容器的内核功能

传统UNIX仅实现了特权和非特权两类进程,前者是指以0号UID身份运行的进程,而后者则是从属非0号UID用户的进程。Linux内核从2.2版开始将附加于超级用户的权限分割为多个独立单元,这些单元是线程级别的,它们可配置在每个线程之上,为其赋予特定的管理能力。Linux内核常用的功能包括但不限于如下这些。

▪CAP_CHOWN:改变文件的UID和GID。

▪CAP_MKNOD:借助系统调用mknod()创建设备文件。

▪CAP_NET_ADMIN:网络管理相关的操作,可用于管理网络接口、netfilter上的iptables规则、路由表、透明代理、TOS、清空驱动统计数据、设置混杂模式和启用多播功能等。

▪CAP_NET_BIND_SERVICE:绑定小于1024的特权端口,但该功能在重新映射用户后可能会失效。

▪CAP_NET_RAW:使用RAW或PACKET类型的套接字,并可绑定任何地址进行透明代理。

▪CAP_SYS_ADMIN:支持内核上的很大一部分管理功能。

▪CAP_SYS_BOOT:重启系统。

▪CAP_SYS_CHROOT:使用chroot()进行根文件系统切换,并能够调用setns()修改Mount名称空间。

▪CAP_SYS_MODULE:装载内核模块。

▪CAP_SYS_TIME:设定系统时钟和硬件时钟。

▪CAP_SYSLOG:调用syslog()执行日志相关的特权操作等。

系统管理员可以通过get命令获取程序文件上的内核功能,并可使用setcap命令为程序文件设定内核功能或取消(-r选项)其已有的内核功能。而为Kubernetes上运行的进程设定内核功能则需要在Pod内容器上的安全上下文中嵌套capabilities字段,添加和移除内核能力还需要分别在下一级嵌套中使用add或drop字段。这两个字段可接受以内核能力名称为列表项,但引用各内核能力名称时需移除CAP_前缀,例如可使用NET_ADMIN和NET_BIND_SERVICE这样的功能名称。

下面的配置清单(securitycontext-capabilities-demo.yaml)中定义的Pod对象的demo容器,在安全上下文中启用了内核功能NET_ADMIN,并禁用了CHOWN。demo容器的镜像未定义USER指令,它将默认以root用户的身份运行容器应用。


apiVersion: v1
kind: Pod
metadata:
  name: securitycontext-capabilities-demo
  namespace: default
spec:
  containers:
  - name: demo
    image: ikubernetes/demoapp:v1.0
    imagePullPolicy: IfNotPresent
    command: ["/bin/sh","-c"]
    args: ["/sbin/iptables -t nat -A PREROUTING -p tcp --dport 8080 -j 
    REDIRECT --to-port 80 && /usr/bin/python3 /usr/local/bin/demo.py"]
    securityContext:
      capabilities:
        add: ['NET_ADMIN']
        drop: ['CHOWN']

容器中的root用户将默认映射为系统上的普通用户,它实际上并不具有管理网络接口、iptables规则和路由表等相关的权限,但内核功能NET_ADMIN可以为其开放此类权限。但容器中的root用户默认就具有修改容器文件系统上的文件从属关系的能力,而禁用CHOWN功能则关闭了这种操作权限。下面创建该Pod对象并运行在集群上,来验证清单中的配置。


~ $ kubectl apply -f securitycontext-capabilities-demo.yaml                                
pod/securitycontext-capabilities-demo created

而后,检查Pod网络名称空间中netfilter之上的规则,清单中的iptables命令添加的规则位于NAT表的PREROUTING链上。下面的命令结果表示iptables命令已然生成的规则,NET_ADMIN功能启用成功。


$ kubectl exec securitycontext-capabilities-demo -- iptables -t nat -nL PREROUTING 
Chain PREROUTING (policy ACCEPT)
target     prot    opt    source      destination         
REDIRECT   tcp  --  0.0.0.0/0         0.0.0.0/0       tcp dpt:8080 redir ports 80

接着,下面用于检查demo容器中的root用户是否能够修改容器文件系统上文件的属主和属组的命令结果表示,其CHOWN功能已然成功关闭。


$ kubectl exec securitycontext-capabilities-demo -- chown 200.200 /etc/hosts
chown: /etc/hosts: Operation not permitted
command terminated with exit code 1

内核的各项功能均可按其原本的意义在容器的安全上下文中按需打开或关闭,但SYS_ADMIN功能拥有内核中的许多管理权限,实在太过强大,出于安全方面的考虑,用户应该基于最小权限法则组合使用内核功能完成容器运行。

4.4.4 特权模式容器

相较于内核功能,SYS_ADMIN赋予了进程很大一部分的系统级管理功能,特权(privileged)容器几乎将宿主机内核的完整权限全部开放给了容器进程,它提供的是远超SYS_ADMIN的授权,包括写操作到/proc和/sys目录以及管理硬件设备等,因而仅应该用到基础架构类的系统级管理容器之上。例如,使用kubeadm部署的集群中,kube-proxy中的容器就运行于特权模式。

提示

我们可以将特权容器理解为拥有宿主机root用户权限的容器,这显然严重违背了容器的隔离原则。

下面的第一个命令从kube-system名称空间中取出一个kube-proxy相关的Pod对象名称,第二个命令则用于打印该Pod对象的配置清单,限于篇幅,这里仅列出了其中一部分内容:


~$ pod-name=$(kubectl get pods -l k8s-app=kube-proxy -n kube-system \
         -o jsonpath={.items[0].metadata.name})
~$ kubectl get pods $pod-name -n kube-system -o yaml
#从命令结果中截取的启动容器应用的命令及传递的参数
containers:
  - command:
    - /usr/local/bin/kube-proxy
    - --config=/var/lib/kube-proxy/config.conf
    - --hostname-override=$(NODE_NAME)
    image: ……
    imagePullPolicy: IfNotPresent
    name: kube-proxy
    resources: {}
    securityContext:
      privileged: true

上面保留的命令结果的最后两行是定义特权容器的格式,唯一用到的privileged字段只能嵌套在容器的安全上下文中,它使用布尔型值,true表示启用特权容器机制。

4.4.5 在Pod上使用sysctl

Linux系统上的sysctl接口允许在运行时修改内核参数,管理员可通过/proc/sys/下的虚拟文件系统接口来修改或查询这些与内核、网络、虚拟内存或设备等各子系统相关的参数。Kubernetes也允许在Pod上独立安全地设置支持名称空间级别的内核参数,它们默认处于启用状态,而节点级别内核参数则被认为是不安全的,它们默认处于禁用状态。

截至目前,仅kernel.shm_rmid_forced、net.ipv4.ip_local_port_range和net.ipv4.tcp_syncookies这3个内核参数被Kubernetes视为安全参数,它们可在Pod安全上下文的sysctl参数内嵌套使用,而余下的绝大多数的内核参数都是非安全参数,需要管理员手动在每个节点上通过kubelet选项逐个启用后才能配置到Pod上。例如,在各工作节点上编辑/etc/default/kubelet文件,添加如下内容以允许在Pod上使用指定的两个非安全的内核参数,并重启kubelet服务使之生效。


KUBELET_EXTRA_ARGS='--allowed-unsafe-sysctls=net.core.somaxconn,net.ipv4.ip_unprivileged_port_start'

net.core.somaxconn参数定义了系统级别入站连接队列最大长度,默认值是128;而net.ipv4.ip_unprivileged_port_start参数定义的是非特权用户可以使用的内核端口起始值,默认为1024,它限制了非特权用户所能够使用的端口范围。

下面配置清单(securitycontext-sysctls-demo.yaml)中定义的Pod对象在安全上下文中通过sysctls字段嵌套使用了一个安全的内核参数kernel.shm_rmid_forced,以及一个已经启用的非安全内核参数net.ipv4.ip_unprivileged_port_start,它将该非安全内核参数的值设置为0来允许非特权用户使用11024以内端口的权限。


apiVersion: v1
kind: Pod
metadata:
  name: securitycontext-sysctls-demo
  namespace: default
spec:
  securityContext:
    sysctls:
    - name: kernel.shm_rmid_forced
      value: "0"
    - name: net.ipv4.ip_unprivileged_port_start
      value: "0"
  containers:
  - name: demo
    image: ikubernetes/demoapp:v1.0
    imagePullPolicy: IfNotPresent
    securityContext:
      runAsUser: 1001
      runAsGroup: 1001

尽管上面配置清单设定了以非特权用户1001的身份运行容器应用,但受上面内核参数的影响,非管理员用户也具有了监听80端口的权限,因而不会遇到无法监听特权端口的情形。下面将配置清单中定义的资源创建在集群之上,来验证设定的结果。


~$ kubectl apply -f securitycontext-sysctls-demo.yaml 
pod/securitycontext-sysctls-demo created

下面的命令结果显示,以普通用户身份运行的demo容器成功监听了TCP协议的80端口。


~ $ kubectl exec securitycontext-sysctls-demo -- netstat -tnlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address  Foreign Address    State   PID/Program name    
tcp        0      0 0.0.0.0:80        0.0.0.0:*      LISTEN    1/python3

需要提醒读者朋友注意的是,在Pod对象之上启用非安全内核参数,其配置结果可能会存在无法预料的结果,在正式使用之前一定要经过充分测试。例如,在某一Pod之上同时配置启用前面示例的两个非安全内核参数可能存在生效结果异常的情况,感兴趣的朋友可自行测试。

本节中介绍了设置Pod与容器安全上下文配置方法及几种常用使用方式。从示例中我们可以看出,设置特权容器和添加内核功能等,以及在Pod上共享宿主机的Network和PID名称空间等,对于多项目或多团队共享的Kubernetes集群存在着不小的安全隐患,这就要求管理员应该在集群级别使用Pod安全策略(PodSecurityPolicy),来精心管控这些与安全相关配置的运用能力。