细节对线,让你写出更安全的 Dockerfile

2023年 7月 25日 26.8k 0

自从进入大容器时代以来,Docker、K8s已逐渐成为开发、测试和部署时不可或缺的工具。如果突然让我不使用Docker,那我可能什么都做不了。但也因为如此,与容器相关的攻击也越来越普遍,因此容器的安全性也变得越来越重要。

想要从零开始构建一个容器,第一步就是要编写 Dockerfile 将你的应用程序打包成 Docker 镜像。关于如何生成尽可能小的镜像已经有很多人写过了,所以今天想要与大家分享的是如何编写一个安全的 Dockerfile,有哪些需要注意的地方。

这篇文章主要关注Dockerfile的安全性,所以不会介绍基本语法。如果你还没有写过Dockerfile,可以先学习了解一些docker知识,然后再回来继续阅读,会更容易理解哦~

使用稳定版或长期支持版的基础镜像

很多人在写 Dockerfile 的时候并不会特别指定基础镜像的版本(就是懒啊,我懂 XD),比如说想要打包一个 Node API 服务器,就直接写 FROM node 或者 FROM node:latest

# Bad

FROM node

WORKDIR /app     
COPY . .

RUN npm install
RUN npm run test

但是这样做可能会导致在某次构建镜像时意外地从 Node 14 升级到 Node 16,从而导致部分功能直接崩溃。而且最新版本的 Node 可能存在一些未知的错误,需要一些勇者来帮忙发现并解决问题,所以除非是自己的副业项目想要尝试最新的功能,否则直接在生产环境中使用最新版本的 Node 并不是一个好的做法

比较好的方式是先看看 Node 的 LTS(Long-Term Support) 版本是多少,像我在写文章的当下是 v14.17.5 ,那就选择 node:14 或是 node:14.17 作为 base image

# Good

FROM node:14

WORKDIR /app   
COPY . .        

RUN npm install 
RUN npm run test

这样做的好处有两个:一是可以将版本号固定在Node 14上,确保不会有大的变动;二是LTS版本会持续推出安全修复,所以如果有一天Node更新到 v14.17.10 ,那些安全修复也会在部署API服务器时被加入。我们只需要等待更新即可。

在安装软件包时,需要指定版本

这一点和上面提到的不要使用最新的图像有些相似,无论你是使用 apt-get install 安装CLI工具,还是使用 npm install 安装库,或者使用curl/wget下载并编译东西,都要尽量确保每次下载到的东西是一样的

比如说,在使用 apt-get 安装 nginx 的时候,可以通过 apt-get install nginx=1.14.0 来下载指定版本(有点麻烦对吧,我也觉得XD)。而像 npm、pip 这类的语言包管理工具,则是根据官方推荐的方法来操作,比如 npm 就是使用 package-lock.json 来锁定包的版本,pip 则是先运行 pip freeze > requirements.txt 将包的版本冻结,等需要安装时再运行 pip install -r requirements.txt 将原本的包安装回来

虽然将套件版本锁定后可以避免很多麻烦,但也不能一直停留在那里不更新。所以记得偶尔去检查一下版本是否过旧,如果是的话,只需手动将版本号升级即可

 只复制需要的东西

平时写 Dockerfile 的时候,有些人为了一时方便,会直接使用 COPY . /app 将整个项目文件夹复制到容器的 /app 文件夹内。这样做虽然不需要费太多心思,在容器内也可以直接访问所有文件,但这种做法可以说是相当糟糕的

首先,这样做会让图片变得非常庞大(连 node_modules 都包含在内,能不庞大吗XD)。而且一不小心就会将敏感数据如 .envrc 一同放入其中。如果有一天黑客获取了这个图片,其中的AWS凭证、数据库密码等极其机密的信息就会直接泄露出去。甚至有可能突然遭受数据库删除的风险

为了避免这种情况发生,在构建图像时应该只复制所需的内容进去,比如说你马上就要跑的时候,才把 npm install 和 package.json 放进去,而代码也只放进去真正会执行的部分就可以了

而且这样还有另外一个好处,就是如果你修改了 src 里面的代码,但没有安装新的 package(开发时大部分都是这样吧~),因为 Docker 会自动进行缓存,所以就不需要重新运行一次 npm install,会直接从第 10 行的 npm run test 开始运行,因此可以大幅缩减需要等待的时间

使用多阶段构建,丢弃不需要的文件

这跟上一点有点类似,简单来说就是不要在镜像里留下任何不需要的东西(没用的东西都给我滚),即便那是构建镜像过程中产生的东西也是一样

举个例子,原本 Go API 服务器的 Dockerfile 可能是这样的,因为在构建镜像时需要编译可执行文件,所以第 5 行的 COPY *.go 是必需的,没有它们就无法进行编译

但说真的一旦编译出执行文件之后,那些 Go 程序代码就用不到了,所以应该来个爽快的过河拆桥,用 multi-stage build 把编译完的执行文件保留下来就好,程序代码什么的就直接拜拜

经过多阶段构建后,下面这个 Dockerfile 中的镜像只包含已编译好的可执行文件,没有任何源代码和 Go 编译器,非常纯粹

那这样有什么好处呢?除了图片可以变小很多之外,即便图片被黑客拿到了,程序代码也不会外泄出去(有很多攻击都是拿到程序代码后从里面找到漏洞),因此只保留可执行文件可以提高安全性

除此之外,由于环境越复杂就越可能存在未发现的漏洞,将基础镜像从原本的 golang:1.17 替换为谷歌提供的 distroless 镜像正好能够大幅减少环境的复杂性(distroless 几乎没有安装任何东西,甚至连 shell 都没有),从而提高安全性

请不要将敏感资料硬编码在Dockerfile文件中

我觉得这已经是常识级别的安全知识了,因为直接把敏感资料写在Dockerfile里会让黑客轻易拿到(只要拿到镜像就可以了),所以绝对不要想不开把数据库或任何的账号密码写在里面,最多用来设置时区或是这种被公开也不会出问题的变量就好,否则哪天数据被偷走真的会哭出来

如果说 ENV 不能放敏感资料,那这些资料究竟要怎么被加进环境变量呢?

答案就是在代码中加上 docker run 或 --env ,或者将环境变量传递进去;如果使用docker-compose,那么将这些数据写入 docker-compose.yml 的环境变量中,这样容器启动时就能读取这些变量,即使镜像被盗也不用担心数据泄露

 弱点扫描

在完成所有可行的任务之后,接下来就是使用工具进行弱点扫描。由于有很多用于弱点扫描的工具,这里介绍一下已经被 Docker 加入 CLI 的 Snyk。它可以将您的环境和安装的软件包发送到他们的数据库进行搜索,以查找潜在的危险

举个例子,我手头有几个多年前用 Node.js 写的 express server,Dockerfile 长这样(看到 9.2.0 就知道真的是多年前XD)。

先用 docker build . -t app 把图像建立起来,然后对其进行 docker scan app 扫描。由于我使用的是古老的 node:9.2.0 ,所以仅仅对基础图像进行扫描就发现了1039个漏洞,其中有99个属于关键级别的,真是吓人

除了告诉你有多少漏洞之外,他还会把每个漏洞列出来(有兴趣可以去读一下那些漏洞的报告,其实都不长),并且告诉你这些漏洞分别在哪些版本修复了

image.png

如果你不想看那些漏洞的话,也可以直接滑到最底下看他给你的建议。比如说,他建议将基础镜像升级到 node:16.7.0 。如果不一定需要完整镜像的话,那么 node:16-bullseye-slim 也是一个不错的选择。因为安装的东西更少,所以漏洞自然也会更少

image.png

 总结

今天介绍了一些在写 Dockerfile 时的注意事项,虽然很多都是小地方,但毕竟魔鬼藏在细节里,想要让你的 Docker image 更安全,那就连这些小细节都不能放过

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论