[docker] docker 安全知识 - docker api, 权限提升 & 资源管理
这是 docker 安全的最后一篇
暴露 docker api
在 [docker] docker 安全知识 - docker 系统性简介 中曾经提到,docker cli 使用 restful api 与客户端和 docker daemon 之间交流。默认情况下,docker cli 和 docker api 通过 unix socket /var/run/docker.sock
进行交流,而 docker 引擎在宿主机器上默认是有 root 权限的
这是之前 curl 的 socket 的结果
docker daemon 是核心的 server,server+rest API + client 加起来是 docker engine
在有些情况下,也会遇到需要将 docker api 使用的 socket 暴露到网络上的情况,如:
-
当容器在不同的宿主机器上运作
这个情况下则需要 query 其他宿主机器的 docker 情况以便实施下一步操作,特别是容器之间有依赖关系的情况
-
集中化容器管理
-
CI/CD
-
监控&日志
-
…
在这种情况,其他人就可以通过暴露出来的 docker api 对容器和镜像进行管理,从而获取对 docker daemon 的操作权限——搭配上 docker 引擎是以 root 权限在宿主机器上运作的这一情况,这就意味着可以调用 docker api 的用户有可能通过暴露 docker api,获取 root 权限。这种漏洞叫 特权提升,后面也会有一个部分会专门讲 特权提升 的常见出发点情况
docker 官方文档是对暴露 socket 有警告的:
毕竟用户通过访问 socket,就有可能获取当前宿主上的文件夹和文件,这也是之前跑有漏洞的镜像时的截图:
在这个情况下,用户可以访问到 /var/lib/docker
这个路径——虽然在 mac 和 windows 上,这可能是虚拟机所在的位置。不过如果是 linux 服务器的话,那就是服务器所在的位置——再重复一遍,在 linux 环境下,docker 并不会重新创建一个虚拟机,而是会使用宿主机器上的 kernal,从而提升性能
换言之,用户也可以直接访问到 /var
下的文件与文件夹,并借助 docker api,以 root 权限在 /var
下进行操作
不安全的配置
下面会列举几种将 socket 暴露出来的方式
-
docker cli
❯ sudo dockerd -H unix:///var/run/docker.sock -H tcp://192.168.59.106:2375
这是一个临时地暴露端口的方法,无法持久化
执行完该命令后,任何可以访问
192.168.59.106:2375
就可以访问通过unix:///var/run/docker.sock
暴露的dockerd
-
使用 systemd service
[Service] ExecStart= ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock -H tcp://192.168.59.106:2375
这个配置和上面的指令一样,不过将其暴露成了一个 service,因此可以持久化
-
通过 daemon.json
{ "hosts": ["unix:///var/run/docker.sock", "tcp://192.168.59.106:2375"], "experimental": true, "features": { "buildkit": true } }
这是通过 docker daemon 实现的暴露
执行了上面任何一个指令后,就可以监听到被暴露的端口了,运行结果大致如下:
❯ sudo netstat -lntp | grep dockerd
tcp 0 0 192.168.59.106:2375 0.0.0.0:* LISTEN 8745/dockerd
curl 的结果大体如下:
❯ curl http://192.168.59.106:2375/containers/json | jq
[
{
"Id": "8dfafdbc3a40",
"Names": [
"/registry-server"
],
"Image": "registry:2",
"ImageID": "sha256:2d4f4b5309b1e65c4a8b6265ffaecc58b239c89973864a7308bda2c95515c5b2",
"Command": "/entrypoint.sh /etc/docker/registry/config.yml",
"Created": 1621859727,
"Ports": [
{
"IP": "0.0.0.0",
"PrivatePort": 5000,
"PublicPort": 5000,
"Type": "tcp"
}
],
"Labels": {
"maintainer": "Docker <support@docker.com>"
},
"State": "running",
"Status": "Up 3 hours",
"HostConfig": {
"NetworkMode": "default"
},
"NetworkSettings": {
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "7ea29fc14125a32c4f66af3c258854d9f72f4961d6903d673b56fbd51581cb1b",
"EndpointID": "d865b3c89d2f06036a8d53133974be4c809cdc1b667a3b3a0a891f62396e92bd",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:02",
"DriverOpts": null
}
}
},
"Mounts": [
{
"Type": "bind",
"Source": "/mnt/registry",
"Destination": "/var/lib/registry",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
]
}
]
一旦可以被 curl 到,也就代表着可以通过扫描软件扫到端口,之前曾经提到的一个 Nmap 就是这样的工具,其运行结果大体如下:
如果别人运行了类似的软件去扫对应的网址,那么被暴露的 dockerd
也会显示在这里
这里最好的解决方法当然是不暴露 dockerd
,不过如果需求是一定要暴露的话,那么解决方案就是添加安全连接的方式,如使用 SSH/TLS
特权提升
特权提升有两种:
-
水平提升
这种情况下,同等级的信息会被获取
如之前提到的使用 Nmap 去探查被暴露在当前域名上的端口
另外一个看到过是在钓鱼邮件里看到的案例,比如说钓鱼邮件被 cc 给了群组,群组又有人点击/回复了邮件,那么就通过群组(比如说 hr@example.com 这种常见群组)获取了个人信息
-
垂直提升 (docker 最常见的问题)
bind mounts
之前在 [docker] 多容器项目 - PHP+MySQL+Nginx+utility containers 案例里比较详细地说明了怎么使用 bind mounts,不在本地安装 php、mysql 实现开发一个 laravel 网站,而这里利用的就是容器修改的文件映射到本地,而本地的修改同样也会映射到容器中
这里就会出现一个问题,一个容器中出现了 bind mounts,那就可能会出现安全隐患。如下面这个案例,容器在运行时没有使用合适的 flag 去传输 secrets,而是暴力的使用 bind mounts 绑定了 secrets:
# 假设可以访问到容器
# 可以看到这里所有的权限都是 root 权限
/app # ls -la /
total 64
drwxr-xr-x 1 root root 4096 Apr 30 03:52 .
drwxr-xr-x 1 root root 4096 Apr 30 03:52 ..
-rwxr-xr-x 1 root root 0 Apr 30 03:52 .dockerenv
drwxr-xr-x 4 root root 128 Apr 30 03:38 app
# 这个指令可以查看所有的 mounts
/app # cat /proc/mounts
# 省略若干行,注意到这里的 bind mounts
/run/host_mark/Users /app fakeowner rw,nosuid,nodev,relatime,fakeowner 0 0
/app # ls
image.dockerfile secrets
/app # ls secrets/
id_rsa
/app # cat secrets/id_rsa
some random text
如果这里真的是使用当前的 secrets 去进行登录的话,那么:
-
就能够获取 host machine 的登录信息
-
可以利用 bind mounts 的特性,新建一个文件,其中包含自己的登录信息
因为 bind mounts 同样也会反映到宿主机器上去,因此同样的密钥也会被添加到宿主机器
换言之,现在黑客就能够使用自己的密钥,顺利的登录宿主机器
这种问题,平时在检测的时候也比较容易发现:
-
运行时是否使用了 bind mounts,这里通过
-v <绝对路径>:路径
的方式可以检测出来# may need to check the file sharing permission ❯ docker run -p 3000:80 -d --rm --name feedback-app # bind mounts # 区别在于这是绝对路径 -v "$(pwd):/app" feedback-node:volumes
-
docker compose 是否绑定了绝对路径
server: image: "nginx:stable-alpine3.17" ports: - "8000:80" volumes: # 这两个都是 bind mounts - ./src:/var/www/html:delegated - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
解决方式就比较简单粗暴了——尽量使用 volume 代替 bind mounts
将 docker api 挂载到容器上
这个实现的安全隐患,和第一点暴露 docker api 比较相似,会这么做的主要原因也是为了方便管理,比如说有一个叫做 Portainer 的容器,可以可视化管理 docker 容器,下面是来自官网教程的动图:
可以看到,如果是一些做未来会交付出去,给非开发进行管理/维护的项目,就可能会使用这种方法——这也是无法避免一定会要使用这种容器的理由。这种情况下,使用 SSH/TLS/VPN 建立安全的登录是最好的防护方式
另一种解决方式是使用 non-root 用户,这里在官方文档也有介绍,这里不多赘述:https://docs.docker.com/engine/security/rootless/
特权容器
类似的我之前跑过:
❯ docker run -it --privileged --pid=host image-vulnerability nsenter -t 1 -m -u -n -i sh
默认情况下,容器是无法访问宿主机的设备(USB, GPU 等),但是使用 --privileged
这个 flag 可以获取这些权限,除此之外还可以挂载文件系统、越过 FS 的权限管理等,除此之外,设备也可以被 map 到容器上去。
对于这个漏洞的检测方式主要有以下几种:
-
特权模式,也就是
--privileged
flag -
添加 capabilities,也就是
--cap-add=CAPABILITY_NAME
flag这里包含的 capability 有以下几个选择:
SYS_ADMIN
,即系统管理NET_ADMIN
,即网络管理SYS_TIME
,即系统时间SYS_PTRACE
, 即ptrace
,可以追踪一些 process
-
添加设备,即
--device
flag
kernel 漏洞
这个是与 linux 镜像有关的一些漏洞了,具体可以在这个网站上进行查询:https://www.exploit-db.com/
一些漏洞包括:
这个就没什么好说的了吧,就是涉及到了底层用的 linux 的漏洞,linux 服务器的话,就和当前的系统有关
资源管理
默认情况下,容器的运行是没有资源限制的,换言之,一个容器很有可能会耗尽当前服务器上所有的资源。以之前提到的一个 package 为例:everything。如果前端容器 不小心 安装了类似的包,那么就会下载 npm registry 上的所有包
这会使用大量的资源,包括带宽和内存。在没有任何限制的情况下,光是前端容器就能够耗尽服务器的资源。当系统内存不够了,就会宕机,这同样也会影响到运行在同样服务器上的其他容器,从而导致整个项目挂掉
限制的方法有:
-
cpu
-
使用,
--cpu-shares=512
-
quota,
--cpu-quota=50000
-
周期,
--cpu-period=100000
-
-
内存
- 大小限制,
-m 512m
- swap,
--memory-swap=1g
- 大小限制,
-
IO
- block,
--blkio-weight=500
- 读写速度,
--device-read-bps /dev/sda:1mb --device-write-bps /dev/sda:1mb
- block,
-
带宽,
--net=my_bandwidth_limited_network
reference
-
Protect the Docker daemon socket
-
Run the Docker daemon as a non-root user (Rootless mode)