大家好,我是Edison。
最近在公司搭建CI流水线,涉及到容器镜像安全的话题,形成了一个笔记,分享与你,也希望我们都能够提高对安全的重视。
近年来应用程序逐步广泛运行在容器内,容器的采用率也是逐年上升。
根据 Anchore 发布的《Anchore 2021年软件供应链安全报告》显示容器的采用成熟度已经非常高了,65% 的受访者表示已经在重度使用容器了,而其他 35% 表示也已经开始了对容器的使用:
因此,基于软件的交付变成了基于容器镜像的交付。
业界已经达成共识:云原生时代已经到来,如果说容器是云原生时代的核心,那么镜像应该就是云原生时代的灵魂。镜像的安全对于应用程序安全、系统安全乃至供应链安全都有着深刻的影响。
但是,容器的安全问题却是大多数IT开发团队所忽视的:
根据 snyk 发布的 2020年开源安全报告 中指出,在 dockerhub 上常用的热门镜像几乎都存在安全漏洞,多的有上百个,少的也有数十个。具体数据如下图所示:
不幸的是,很多应用程序的镜像是以上述热门镜像作为基础镜像,进而将这些漏洞带到了各自的应用程序中,增加了安全风险。
2 解决方式
GitLab(极狐)建议我们:预防为主,防治结合的方式来提高镜像的安全性。
所谓防,就是要在编写 Dockerfle 的时候,遵循最佳实践来编写安全的Dockerfile;还要采用安全的方式来构建容器镜像;所谓治,即要使用容器镜像扫描,又要将扫描流程嵌入到 CI/CD 中,如果镜像扫描出漏洞,则应该立即终止CI/CD Pipeline,并反馈至相关人员,进行修复后重新触发 CI/CD Pipeline。
3 防的最佳实践
3.1 以安全的方式构建容器镜像
常规构建容器镜像的方式就是 docker build,这种情况需要客户端要能和 docker守护进程进行通信。对于云原生时代,容器镜像的构建是在 Kubernetes 集群内完成的,因此容器的构建也常用 dind(docker in docker)的方式来进行。
众所周知,dind 需要以 privilege 模式来运行容器,需要将宿主机的 /var/run/docker.sock 文件挂载到容器内部才可以,否则会在 CI/CD Pipeline构建时收到错误。
为了解决这个问题,可以使用一种更安全的方式来构建容器镜像,也就是使用 kaniko。kaniko是谷歌发布的一款根据 Dockerfile 来构建容器镜像的工具。kaniko 无须依赖 docker 守护进程即可完成镜像的构建。其和GitLab CI/CD的集成也是非常方便的,只需要在GitLab CI/CD 中嵌入即可,下面是在我司CI Pipeline中的实践:
variables: EXECUTOR_IMAGE_NAME: "gcr.io/kaniko-project/executor" EXECUTOR_IMAGE_VERSION: "debug" docker-build-job: stage: docker-build-stage image: name: "$EXECUTOR_IMAGE_NAME:$EXECUTOR_IMAGE_VERSION" entrypoint: [""] rules: - if: '$IMAGE_SOURCE_BUILD != "" &&$BUILD_DOCKER_IMAGE == "true" && $CI_PIPELINE_SOURCE !="merge_request_event"' script: - |- KANIKO_CONFIG="{\"auths\":{\"$CI_REGISTRY_IMAGE\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" echo "${KANIKO_CONFIG}" >/kaniko/.docker/config.json - mkdir release - cp -r Build/* release/ - | /kaniko/executor \ --context "${CI_PROJECT_DIR}" \ --dockerfile "Dockerfile" \ --destination"${CI_REGISTRY_IMAGE}:${BUILD_TAG}"
3.2 选择合适且可靠的基础镜像
Dockerfile 的第一句通常都是 FROM some_image,也就是基于某一个基础镜像来构建自己所需的业务镜像,基础镜像通常是应用程序运行所需的语言环境,比如.NET、Go、Java、PHP等,对于某一种语言环境,一般是有多个版本的。
我司主要使用的是.NET,而原生微软的ASP.NET 6.0镜像(mcr.microsoft.com/dotnet/aspnet:6.0)有5个Critical的安全漏洞,一般不建议采用。根据Global项目组的实践,建议采用RedHat提供的.NET 6.0运行时镜像,该镜像由RedHat维护,定期在更新(最新更新是一周前),目前无Critical的安全漏洞。
镜像地址:点此浏览
docker pull registry.access.redhat.com/ubi8/dotnet-60-runtime:6.0-22
3.3 不安装非必要的安装包
Dockerfile 中应该尽量避免安装不必要的软件包,除非是真的要用到。比如:我们习惯了直接写 apt-get update && apt-get install xxxx。
因为,安装非必要的软件包除了会造成镜像体积的增大 也会 增加受攻击的风险程度。
3.4 以非root用户启动容器
在 Linux 系统中,root用户意味着超级权限,能够很方便的管理很多事情,但是同时带来的潜在威胁也是巨大的,用 root 身份执行的破坏行动,其后果是灾难性的。在容器中也是一样,需要以非root 的身份运行容器,通过限制用户的操作权限来保证容器以及运行在其内的应用程序的安全性。在 sysdig 发布的《Sysdig 2021年容器安全和使用报告》中显示,58% 的容器在以 root 用户运行。足以看出,这一点并未得到广泛的重视。
因此,建议在Dockerfile中添加命令来让容器以非root用户身份启动,在我司的CI Pipeline中的实践:
...... USER 0 RUN chown -R 1001:0/opt/app-root && fix-permissions /opt/app-root # No root should run USER 1001 ENV ASPNETCORE_URLS=http://+:8080 EXPOSE 8080 CMD dotnet ${APPLICATION_DLL}
备注:上面的${APPLICATION_DLL}是在镜像打包阶段由流水线通过参数传递给Dockerfile的。
4 治的最佳实践
治的最佳实践就是:在CI流水线中加入容器镜像安全扫描任务。
在 GitLab 中提供了容器镜像分析器(Container-Scanning-Analyzer)来对生成的容器镜像进行扫描,建议将其加入CI Pipeline中进行高频率的检查工作。在我司的CI Pipeline中,集成了container-scanning-analyzer来扫描容器镜像,如果扫描结果有Critical的漏洞,流水线会自动失败,阻塞后续Job执行并发送Email提醒。下图给出了一个简单的示例(并非我司CI流水线完整流程):
只有当扫描结果不包含Critical的漏洞时,流水线才会被视为成功,进而允许后续操作,包括Merge开发分支到主干等。
参考资料
极狐:《GitLab DevSecOps七剑下天山之容器镜像安全扫描》
极狐:《云原生时代,如何保证容器镜像安全?》