[docker] docker 安全知识 - 镜像,port & registry
这是第一篇,安全部分还有一篇笔记就记完了
说实话,看完了要学的这些东西,感觉大多数安全问题都可以通过验证登录的合法性去解决
镜像
镜像的问题还是比较多的,主要问题也是集中在:
-
镜像的可变性
这是针对使用了 恶意镜像(malicious images) 这一情况
-
镜像的不可变性
这是针对当前镜像使用了 不安全的镜像(images with vulnerabilities) 这一情况
-
镜像的 layer 结构
这个是 docker 为了在建造镜像的过程中提速而实现的缓存机制,从而可能会导致信息被嵌入 layer 中,从而被暴露——只要获取镜像,就能够查看所有的 layers
需要注意,恶意镜像和不安全的镜像二者有这本质的区别:
前者是黑客 有意 嵌入具有攻击性的代码,从而威胁到镜像的安全性
后者是开发 无意 实现的 bug
恶意镜像
一般来说正常下载镜像的时候都会使用镜像名+tag 的方式,如果只是想拉一个最新版本,那么也会忽略 tag,如:
❯ docker run node
Unable to find image 'node:latest' locally
# automically pull the latest version from docker hub
latest: Pulling from library/node
609c73876867: Downloading [========================> ] 24.25MB/49.56MB
7247ea8d81e6: Download complete
be374d06f382: Download complete
b4580645a8e5: Downloading [==> ] 10.19MB/211.1MB
dfc93b8f025c: Waiting
a67998ba05d7: Waiting
9513f49617f6: Waiting
e2a102227dc6: Waiting
或者是用 docker-compose:
server:
image: "nginx:stable-alpine3.17"
ports:
- "8000:80"
volumes:
- ./src:/var/www/html:delegated
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
乍一看好像没什么问题,但是如果跑过几次 docker build
的话就会注意到一件事情——镜像+tag
这一组合,其实是可以包含不同的镜像的,如:
❯ docker build -f image.dockerfile -t image-vulnerability .
# 省略步骤若干
=> [5/5] RUN rm /root/.ssh/id_rsa 0.2s
=> exporting to image
=> => exporting layers 0.0s
=> => writing image sha256:1a32b91cf2968f0431c6b357b91f0af97c3ad20a4243743fc830f38eb6e3d769 0.0s
=> => naming to docker.io/library/image-vulnerability
❯ docker build -f image.dockerfile -t image-vulnerability .
# 省略步骤若干
[+] Building 0.4s (11/11) FINISHED docker:desktop-linux
=> [internal] load build definition from image.dockerfile 0.0s
=> [6/6] RUN echo "build completed" 0.1s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:2ffc6854752a07eb98cf5f83c775bb79b9d39ee50a1713037248b679d5af1031 0.0s
=> => naming to docker.io/library/image-vulnerability
这里我重新修改了一下 Dockerfile,所以运行的结果会不一样,打包的 image 不一样:
- 前者的 id 为 sha256:1a32b91cf2968f0431c6b357b91f0af97c3ad20a4243743fc830f38eb6e3d769
- 后者的 id 为 sha256:2ffc6854752a07eb98cf5f83c775bb79b9d39ee50a1713037248b679d5af1031
但是二者共享的是一个镜像名,即 image-vulnerability:latest
,如果只是拉去 image-vulnerability:latest
,那么无法判断使用的究竟是哪一个镜像。因此,这里的情况就是:
对于恶意镜像比较常见的处理方式有:
- 选择验证过的镜像
- 选择 docker content trust 镜像
- 选择不可变的 hash 值去下载镜像
以 docker hub 为例,尽量选择官方支持/验证的镜像:
使用有漏洞的镜像
这里有漏洞的镜像可以被两个组件使用:
-
docker 镜像本身
如使用 Node 作为基础镜像去搭建服务器
-
应用软件
如一个前后端项目,前端向使用有问题镜像的服务器发送请求
尽管不是所有的漏洞都会被黑客攻击,还是需要了解所使用的镜像包含哪些漏洞,以及根据漏洞等级进行对应的更新
监狱 docker 镜像的不可变性,每一次的更新都会比较具有挑战性,因为需要更新
-
使用有漏洞镜像的容器
包括 dockerfile(更新镜像 A),以及重新打包容器
-
所有和对应容器有依赖关系的容器
还是因为容器的不可变性,无法做到更新了 A,让所有与 A 有引用关系的容器都自动更新
所以这里需要重新打包所有基于 A 的容器
secret
之前在一篇笔记里提到可以使用 ARG
而非 ENV
,不过很不幸地发现,使用 ARG
也不保险。一般常遇到的情况可能有两个:
-
直接使用
ARG
明文这种情况使用
ENV
和ARG
没区别,一样直接通过docker inspect
就能看到明文信息⚠️:Docker 17.07 之前
ARG
是会明文显示在打包历史中的,之后的版本尽管移除了这个设定,使得ARG
的安全性有了提升,不过任何 secrets(密钥相关) 最好还是不要用ARG
写入 docker 镜像中。不当使用依旧会造成 secrets 被写入 docker layers 中,从而使得密钥可以被所有人访问 -
将
ARG
写入文件,再删除掉文件下面的案例就是写入文件的问题——这里不一定需要使用
ARG
,任何情况的写入,包括使用COPY
都会造成这个问题
下面是不当设置密钥的 dockerfile:
FROM node:20-alpine
WORKDIR /app
RUN mkdir -p /root/.ssh
COPY id_rsa /root/.ssh/id_rsa
RUN rm /root/.ssh/id_rsa
这里从当前文件夹复制了一个密钥文件,之后执行了一些操作,最终再删除这个密钥
下面是用 dive 简单地进行了一个命令行的可视化,让案例讲解变得简单一些
在某一层 layer 中可以清楚地看到执行的操作是 COPY id_rsa /root/.ssh/id_rsa
这个时候就可以下载这个镜像,在 本地 打包,构筑一个容器,再使用下面的指令,以管理员的全选对容器进行操作:
❯ docker run -it --privileged --pid=host image-vulnerability nsenter -t 1 -m -u -n -i sh
下面这个截图就是从 overlay2
中获取 id_rsa
的方法了:
👀 最后一行,通过 cat
指令可以轻松获取已经被删除的 id_rsa
文件,这也是因为 overlayer2 的实现,所有的 COPY
、 FUN
、DELETE
操作都是在底层的只读层,而这些只读层是会被持久化的。换言之,文件看起来可能像是被删除了,但是因为这个操作被持久化了,所以还是可以在 overlayer2 找到被删除的文件
这里的使用方法是使用 --mount
, -ssh
和 --secret
使用方法如下:
RUN --mount=type=ssh git clone git@github.com:user/repo.git
这种情况下,docker 会使用宿主机器上的 SSH 配置去下载 repo
❯ docker build --ssh default=id_rea .
这种情况下,docker 也会使用宿主机器上的 SSH 配置去下载 repo
❯ docker build --secret id=mysecret,src=mysecret.txt .
这种用法更多,可以传更多的 secret 而不是只有 SSH
暴露端口
docker 容器的 network 设置其实挺复杂的,默认情况下是 bridge,这种也是用的比较多的情况,但是 network 的类型总共是有 6 种。除了 None
是彻底关闭容器的端口,不需要予以考量之外,其他的 networking 连接方式总是会涉及到 docker 容器的网络配置与宿主机器的网络配置
最简单的可能涉及到端口冲突,比如说 docker 想使用宿主机器上 3000 的端口,不过宿主机器上已经有一个进程占用了 3000,其他更加复杂的情况,鉴于我也不是 network engineer,这里就不过多涉及了
总之简单来说,如果 dockerfile 没有配置好的话,那么就有可能会引起安全问题,其中一个比较常见的情况是未授权访问的情况
一个简单的案例描述如下:
某产品可能使用了 phpMyAdmin 进行 debug,但是在生产环境下,因为没有配置好 dockerfile 使得 phpMyAdmin 的端口暴露了。黑客使用了一些软件,比如说 Nmap,查看了所有连接到当前网址的端口,如:
这个情况下,phpMyAdmin 与当前网址的连接端口就会被暴露了,黑客如果访问了这个端口,并且 phpMyAdmin 又没有设置防护,或是防护加密等级比较低,那么就代表着生产环境的数据就全都暴露在了黑客的眼皮子底下
这种情况下的解决方案也比较简单:
- 生产环境的配置文件一定需要再三检查
- 如非必需,不要暴露端口
- 如果必须要暴露端口,那么使用加密登陆的方式
⚠️:配置是很重要的,暴露端口的 flag 是 -p
,但是同样还有一个 --publish-all
的 flag。使用这个 flag 会保证所有在 dockerfile 中声明的 EXPOSE
端口会被 自动 且 随机 地 map 到宿主机器上的任意端口
虽然感觉是很方便的配置,不过也增加了不可预测性。因为端口是随机配置的,所以对宿主机器上的防火墙配置也有更高的挑战,如果没有配置好,那么也会暴露出更多的安全问题
docker registry
registry 的安全也是可能出现的漏洞之一,如上文提到的恶意镜像,如果黑客没有权限访问到 registry,那么自然也无法下载恶意镜像,从而规避了风险
registry 中,最知名的公开 registry 是 docker hub(也就是官方),对于私有 registry,那么选择就比较多了
下面是使用 nexus 去建立了一个 registry,并且 curl 了一下所有的 docker 镜像,与 curl 一下 docker hub 官方镜像的对比:
❯ curl http://localhost:8081/repository/docker-private-repo/v2/_catalog
{"repositories":[]}%
# 对比 docker hub
❯ curl Https://index.docker.io/v2/_catalog
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Class":"","Name":"catalog","Action":"*"}]}]}
可以看到,curl nexus 的时候不需要任何的验证,直接就返回所有的 repositories,对比 curl docker hub,则是需要验证才能够获取所有的镜像
⚠️:我这里只是新建了一个 nexus 服务器和对应的 docker repository,并没有上传任何镜像,所以返回的结果是空数组:
如果里面真的有镜像的话,那么就会返回所有的镜像,那么这个时候,就可以根据 http://localhost:8081/repository/docker-private-repo/image-name
这样的路径去下载镜像,检查镜像,然后再上传镜像,从而触发 恶意镜像 这个安全隐患
就算实现了一些基础的验证,让黑客无法上传被修改的镜像,但是黑客也可以尝试下载所有的镜像,导致 DoS(Denial of Service)……毕竟会对 registry 添加 load balancer 的操作,还蛮少见的……正常访问都不会触发这种问题
解决方法也挺简单的:
-
验证,验证,还是验证
authentication 真的可以解决七八成的网络安全问题……
-
添加 TLS
-
权限管理
即 authorization,确认只有对应权限的人才分别具有 CRUD 等不同的操作权限
讲道理,docker 的报错 icon 还蛮可爱的……