Docker 的出现改变了应用程序的运行方式与交付模式:应用程序运行在容器内而软件的交付变成了容器镜像的交付。随着这几年云原生的火热,容器的采用率也是逐年上升。根据 Anchore 发布的《Anchore 2021 年软件供应链安全报告》显示容器的采用成熟度已经非常高了,65% 的受访者表示已经在重度使用容器了,而其他 35% 表示也已经开始了对容器的使用:
但是容器的安全问题却不容乐观,这个可以在公众号文章极狐GitLab DevSecOps 七剑下天山之容器镜像安全扫描查看详情。
由于容器是由容器镜像生成的,如何保证容器的安全,在很大程度上取决于如何保证容器镜像的安全。而对于容器镜像安全的保证,可以秉承预防为主,防治结合的理念来进行。所谓防,就是要在编写 Dockerfle 的时候,遵循最佳实践来编写安全的 Dockerfile;还要采用安全的方式来构建容器镜像;所谓治,既要使用容器镜像扫描,又要将扫描流程嵌入到 CI/CD 中,如果镜像扫描出漏洞,则应该立即终止 CI/CD Pipeline,并反馈至相关人员,进行修复后重新触发 CI/CD Pipeline。
下面就从即防又治的角度来讲述如何确保容器镜像安全。
Dockerfile 的第一句通常都是FROM some_image
,也就是基于某一个基础镜像来构建自己所需的业务镜像,基础镜像通常是应用程序运行所需的语言环境,比如 Go、Java、PHP 等,对于某一种语言环境,一般是有多个版本的。以 Golang 为例,即有golang:1.12.9
,也有 golang:1.12.9-alpine3.9
,不同版本除了有镜像体积大小的区别,也会有安全漏洞数量之别。上述两种镜像的体积大小以及所包含的漏洞数量(用 trivy 扫描)对比如下:
可以看到 golang:1.12.9-alpine3.9
比 golang:1.12.9
有更小的镜像体积(351MB vs 814MB),更少的漏洞数量(24 vs 1306)。所以,在选取基础镜像的时候,要做出正确选择,不仅能够缩小容器镜像体积,节省镜像仓库的存储成本,还能够减少漏洞数量,缩小受攻击面,提高安全性。
在 Linux 系统中,root 用户意味着超级权限,能够很方便的管理很多事情,但是同时带来的潜在威胁也是巨大的,用 root 身份执行的破坏行动,其后果是灾难性的。在容器中也是一样,需要以非 root 的身份运行容器,通过限制用户的操作权限来保证容器以及运行在其内的应用程序的安全性。在 Dockerfile 中可以通过添加如下的命令来以非 root 的身份启动并运行容器:
RUN addgroup -S jh && adduser -S devsecops -G j
USER devsecops
上述命令创建了一个名为 jh
的 Group,一个名为devsecops
的用户,并将用户 devsecops
添加到了 jh
Group 下,最后以 devsecops
启动容器。
sysdig 发布的《Sysdig 2021 年容器安全和使用报告》中显示,58% 的容器在以 root 用户运行。足以看出,这一点并未得到广泛的重视。
很多用户在是编写 Dockerfile 的时候,习惯了直接写 apt-get update && apt-get install xxx
,网上也有很多这样的例子(包括 GitHub)。用户需要清楚 xxx 这个包是否真的要用,否则这种情况会造成镜像体积的变大以及受攻击面的增加。
以 ubuntu:20.04
为例来演示安装 vim curl telnet
这三个常用软件包,给镜像体积以及漏洞数量带来的影响:
可以看出,因为安装了 vim curl telnet
这三个常见的软件包,导致镜像体积增加了一倍(从 72.4MB 到 158MB),漏洞数量翻了接近一番(从 60 到 119)。因此,在编写 Dockerfile 的时候,一定要搞清楚哪些包是必须安装的,而哪些包是非必需安装的。不要认为 apt-get install
使用起来很爽就都安装。
针对其他操作系统的包管理器存在同样的问题,诸如 apk add,yum install 等。
多阶段构建不仅能够对于容器镜像进行灵活的修改,还能够在很大程度上减小容器镜像体积,减少漏洞数量(这个第一点有异曲同工之妙)。
由于镜像构建的灵活性和便捷性,任何一个人都可以构建容器镜像并推送至 Dockerhub 供其他人使用。所以在搜索某一个镜像的时候,会出现很多类似的结果,这时候就需要仔细辨别:镜像是否有官方提供的,镜像是否一直有更新,镜像是否可以找到对应的 Dockerfile 来查看到底是如何构建的。信息不全且长时间无更新的镜像,其安全性无法得到保证,不应该使用此类镜像,这时候可以选择自己使用这些规则来构建可用的安全景象。
当然,除此以外,还有很多编写 Dockerfile 的最佳时间,诸如不要把敏感信息编写在 Dockerfile 并构建在镜像中,避免敏感信息造成泄漏;要用工具(如 Hadolint)来对 Dockerfile 进行扫描,以发现 Dockerfile 编写过程中的一些问题等等。
良好的 Dockerfile 编写习惯是保证容器镜像安全的第一步,接下来还需要用安全的方式来构建容器镜像。
常规构建容器镜像的方式就是 docker build
,这种情况需要客户端要能和 docker 守护进程进行通信。对于云原生时代,容器镜像的构建是在 Kubernetes 集群内完成的,因此容器的构建也常用 dind
(docker in docker)的方式来进行。比如在前面所有文章的 Demo 演示中,镜像的构建通常用如下代码:
build:
image: docker:latest
stage: build
services:
- docker:20.10.7-dind
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:1.0.0 .
- docker push $CI_REGISTRY_IMAGE:1.0.0
众所周知,dind
需要以 privilege 模式来运行容器,需要将宿主机的 /var/run/docker.sock
文件挂载到容器内部才可以,否则会在 CI/CD Pipeline 构建时收到如下错误:
因此在使用自建 Runner 的时候,往往都需要挂在 /var/run/docker.sock
,诸如在使用 K3s 来运行极狐GitLab Runner 的时候,就需要在 Runner 的配置文件中添加以下内容:
[[runners.kubernetes.volumes.host_path]]
name = "docker"
mount_path = "/var/run/docker.sock"
host_path = "/var/run/docker.sock"
为了解决这个问题,可以使用一种更安全的方式来构建容器镜像,也就是使用 kaniko。
Kaniko 是谷歌发布的一款根据 Dockerfile 来构建容器镜像的工具。Kaniko 无须依赖 docker 守护进程即可完成镜像的构建。其和极狐GitLab CI/CD 的集成也是非常方便的,只需要在极狐GitLab CI/CD 中嵌入如下代码即可:
build:
stage: build
tags:
- k3s
image:
name: registry.jihulab.com/jh-xiaomage-devops/go-demo/kaniko:debug
entrypoint: [""]
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
- >-
/kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:1.0.0"
代码块说明:
gcr.io/kaniko-project/executor:debug
,本文为了加速构建过程,将此镜像托管在极狐GitLab SaaS上,地址如上述代码块所示;/kaniko/.docker
目录,用来存放登录容器镜像仓库所需的凭证,接下来就是将镜像仓库的登录凭证以 config.json 的格式存放在 /kaniko/.docker
目录下,最后使用 /kaniko/exector
命令来构建容器镜像。
CI/CD Pipeline 的构建日志如下:
上述整个过程是在用 K3s 拉起的极狐GitLab Runner 实例上面运行的此次构建,Runner 的信息可以在 Project --> Settings --> CI/CD --> Runners 里面看到:
在构建日志中也可以看到,此次构建是在 K3s 上运行的 Runner 上进行的:
而用 K3s 来安装极狐GitLab Runner 的配置文件如下:
gitlabUrl: "https://jihulab.com/"
runnerRegistrationToken: "Qif-fakrBBwzXnLUUaxv"
concurrent: 10
checkInterval: 30
logLevel: info
rbac:
create: true
metrics:
enabled: false
runners:
config: |
[[runners]]
[runners.kubernetes]
namespace = "{{.Release.Namespace}}"
image = "ubuntu:20.04"
name: k3s-runner
tags: "jh,k3s,runner"
其中并没有 /var/run/docker.sock
相关的配置。这说明使用 kaniko 来构建容器镜像,并不需要与 docker 守护进程进行通信,所以是以一种更安全的方式完成了容器的构建。
关于如何使用 K3s 来拉起极狐GitLab Runner 实例的内容可以查看文章用 K3s 来安装和运行极狐GitLab Runner。
在遵从最佳实践编写 Dockerfile、用 Kaniko 构建容器之后,还需要对容器镜像做安全扫描,进一步确保容器镜像安全。而极狐GitLab有开箱即用的 DevSecOps 功能,其中就包含容器镜像扫描,关于详细的原理可以查看文章极狐GitLab DevSecOps 之容器镜像安全扫描。
可以很容易的在极狐GitLab CI/CD 中把容器镜像扫描集成进去,以下几行简单命令就可以实现:
include:
- template: Security/Container-Scanning.gitlab-ci.yml
container_scanning:
stage: test
tags:
- k3s
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:1.0.0
参数说明:
触发 CI/CD Pipeline 之后,可以看到构建日志:
最后可以在极狐GitLab的 Security Dashboard 中看到扫描报告:
可以很容易的将镜像构建、镜像扫描集成到极狐GitLab CI/CD Pipeline 中,代码如下:
services:
- docker:20.10.7-dind
stages:
- build
- test
build:
stage: build
tags:
- k3s
image:
name: registry.jihulab.com/jh-xiaomage-devops/go-demo/kaniko:debug
entrypoint: [""]
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
- >-
/kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:1.0.0"
include:
- template: Security/Container-Scanning.gitlab-ci.yml
container_scanning:
stage: test
tags:
- k3s
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:1.0.0
接下来只要提交 MR,就会触发构建流程,扫描的结果会显示在 MR 中:
点击相应的 CVE 就能够直接创建 issue 来对此问题进行跟踪:
等研发人员根据 issue 进行相应的修复之后,再次提交 MR 会继续看到扫描结果,以查看修复是否成功,如果成功修复,则可合并此 MR。
这种将镜像安全扫描嵌入到 CI/CD 中,能够做到持续自动化;将安全与研发工作流结合起来,能够做到安全漏洞可视化,方便研发人员第一时间修复漏洞,做到了真正的“安全左移”。这也是极狐GitLab一体化 DevSecOps 的真正优势:助力用户在一个平台上高效、安全的交付软件。
DevSecOps 功能是极狐GitLab旗舰版专属的,但是用户可以申请免费试用。
选择一个想要使用 DevSecOps 功能的 Group,点击左侧导航栏中的安全,可以看到如下界面并点击开始免费使用:
在出现的表单中输入相应的信息,点击继续:
点击开始免费试用即可:
接着就可以看到 DevSecOps 功能已经开启: