容器镜像是编译构建而成、存储在镜像仓库中、由Docker命令或kubectl命令启动运行的软件包。在容器镜像的构建、保存、获取以及启动环节中,有很多的安全隐患。
对于使用过Docker或Kubernetes的读者而言,对容器镜像都应该是比较熟悉的。首先一个容器镜像由两部分组成的,分别是根文件系统和镜像配置。通常构建一个容器镜像有几个步骤,首先获取一个基础镜像,比如alpine镜像。这个基础镜像可以通过Docker run -it alpine sh来启动。启动完成后可以通过命令进入启动好的容器之中,这个容器里有一套文件系统,这个文件系统就是根文件系统。这个基础镜像并不包含要运行的应用程序,这个时候需要编写Dockerfile,在Dockerfile中通过FROM、ADD、COPY和RUN指令来修改镜像的根文件系统。通过构建Dockerfile所生成的新的镜像就包含满足应用运行所要求的根文件系统了,这个文件系统中就包括了基础的根文件系统和应用的执行程序。
除了根文件系统外,另外一部分就是镜像配置,镜像配置是通过USER、PORT和ENV命令来配置和修改的。修改完成后,通过Docker inspect命令可以查看到镜像的配置信息,下面是使用Docker inspect命令才查看镜像配置的示例。
Docker inspect $IMAGE_ID | grep IPAddress
"IPAddress": "172.17.0.3",
"SecondaryIPAddresses": null,
容器镜像的配置信息是在容器真正运行的时候才被加载的,比如说,通过ENV设置了IP地址,那么这个IP地址会在容器镜像启动过程中由内置的命令将IP设置进去。镜像的配置信息也可以通过Docker run -e <VARNAME>=<NEWVALUE>在容器启动中动态更改。
在Kubernetes中,通过在yaml声明文件中的ENV变量来设置容器的配置信息,下面是Kubernetes中使用ENV的例子。
apiVersion: v1
kind: Pod
metadata:
name: demo
spec:
containers:
- name: demo-container
image: demo-reg.io/some-org/demo-image:1.0
env:
- name: DEMO_ENV
value: "new value"
在上面yaml格式的声明文件中,运行了demo-image镜像,在Pod的启动过程中,注入了一个名为DEMO_ENV的变量名,把它的值设置为“new value”。
了解了容器镜像的分层和配置注入技术之后,接下来我们了解下容器运行时守护进程的安全问题。
Docker守护进程的安全问题
Docker这个词被大范围、多场合提及,在容器技术及云原生技术行业的人都非常的熟悉。在不同的场合,Docker这个词有不同的意义,为了接下来的表述能够清晰,这里先简单地梳理一下Docker这个词的意义。
首先Docker是一家公司,它出品了Docker软件。Docker软件又有Docker运行时和Docker命令行这两大部分。Docker运行时支持运行容器镜像;Docker命令行支持发起编译构建并最终生成容器镜像,也支持调用Docker运行时来运行容器镜像。
一个完整的镜像配置文件包括了运行容器所需要的全部信息,这些信息包括主机名、存储卷、网络信息、虚拟内存等等。当运行Docker命令时,命令行工具本身没有做什么事情,而是直接把命令发送到Docker守护程序中。平台中需要使用Docker守护程序来管理和运行容器,所以Docker守护进程是一个长期运行的进程。Docker守护进程需要以root根用户身份来运行。
在容器的创建过程中,Docker守护程序首先在系统底层创建命名空间(Linux Namespace)。Linux Namespace提供了对系统资源的封装和隔离,处于不同Namespace的进程拥有独立的全局系统资源,改变一个Namespace中的系统资源只会影响当前Namespace里的进程,对其他Namespace中的进程没有影响。Linux内核实现了多种不同类型的Namespace,提供对包括计算和网络在内的不同类型资源的隔离。创建Linux Namespace是一个系统调用,需要使用根用户权限,这是Docker守护进程(docker deamon)需要以root用户来运行的原因。
由Docker命令构建出来的镜像是符合OCI标准的。由于Docker软件工具集的成熟度较高,在常见的使用场景下,使用一台服务器或一个服务器集群作为构建容器映像的服务器资源,并将构建出来的镜像存储在镜像仓库中。构建服务器必须运行Docker守护进程。命令行工具与守护进程之间通过套接字(docker socket)来通信,所以任何能够访问docker socket的应用程序都可以向守护进程发送指令。在没有安全保障的情况下,任何人都可以在这台机器上触发docker build命令。前面讲到过:docker daemon是以root身份运行的,所以能够访问docker socket的用户有能力通过Docker进程运行所有底层指令。此外,如果发生了恶意操作行为,因为这些操作是由容器命令发起的,而不是由某个用户或其他进程发起的,所以很难追踪这些恶意操作的源头,给安全控制带来很大的隐患,这就是Docker守护进程的安全问题,也是docker daemon在安全性方面被诟病的主要原因。
为了避免由Docker命令引发的安全风险,可以使用一些专门的技术工具脱离对docker daemon的依赖,这些技术工具有BuildKit、PodMan、Bazel等。
(1)BuildKit
BuildKit是Docker官方社区推出的下一代镜像构建工具,官方宣称通过BuildKit可以更加快速、有效、安全的构建容器镜像。
BuildKit由Docker公司推出,对Dockerfile有天然较好的支持,它内置高效缓存,支持并行构建操作能力,相比较Docker构建方式,其在执行效率上有明显的优势。另外,BuildKit的安全也较为便捷,仅需要容器运行时就能运行。当前BuildKit所支持的容器运行时有containerd和runc,这两个容器运行时也都是云原生平台首推的两个主流选择。
BuildKit由buildkitd daemon和buildctl两个进程组成,buildkitd需要在平台中预先安装runc或containerd。buildkitd支持非root用户模式运行,可以通过非root用户来运行BuildKit的守护进程,避免了docker daemon的安全问题。
(2)Podman
Podman是Redhat推出的一个无守护容器引擎,通过Podman在Linux系统上开发、管理和运行OCI容器。
Podman的设计理念是完全按照Docker命令的模式来操作,官方甚至推荐直接使用“alias Docker=podman”命令来替换Docker命令行。在Podman的架构设计中,没有采用Docker所采用的客户端/服务器模式,而是采用本地fork/exec模式,在运行的时候主动fork一个进程,所以Podman没有守护进程,通过这种方式,大大提升了容器生命周期中的安全性控制。
(3)Bazel
Bazel是一个功能强大的多语言编译器,可以编译Java、C++、Android、iOS、Golang应用程序,同样也支持容器镜像的编译构建。其原理是通过扩展插件机制,来添加对新语言及新平台的支持。
使用Bazel分为两个步骤,首先是创建一个工作空间,Bazel从这个工作空间里查找编译文件和Bazel运行时所需要的配置文件。之后,创建Bazel所需要的BUILD文件,在BUILD文件中定义了编译构建的执行过程。当Bazel执行构建时,先加载与构建相关的文件,分析其输入和依赖关系,根据指定的规则生成动作图,再根据动作图执行构建操作,直至生产最终的容器镜像。Bazel由谷歌公司开源,在谷歌内部有广泛的使用。
以上内容截取自《云原生安全》
作者:李学峰