[docker] docker 安全知识 - docker api, 权限提升 & 资源管理

发布于:2024-06-04 ⋅ 阅读:(118) ⋅ 点赞:(0)

[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 权限的

crul docker socket

这是之前 curl 的 socket 的结果

docker engine

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 去进行登录的话,那么:

  1. 就能够获取 host machine 的登录信息

  2. 可以利用 bind mounts 的特性,新建一个文件,其中包含自己的登录信息

    因为 bind mounts 同样也会反映到宿主机器上去,因此同样的密钥也会被添加到宿主机器

    换言之,现在黑客就能够使用自己的密钥,顺利的登录宿主机器

这种问题,平时在检测的时候也比较容易发现:

  1. 运行时是否使用了 bind mounts,这里通过 -v <绝对路径>:路径 的方式可以检测出来

    # may need to check the file sharing permissiondocker run
    -p 3000:80
    -d
    --rm
    --name feedback-app
    # bind mounts
    # 区别在于这是绝对路径
    -v "$(pwd):/app"
    feedback-node:volumes
    
  2. 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 到容器上去。

对于这个漏洞的检测方式主要有以下几种:

  1. 特权模式,也就是 --privileged flag

  2. 添加 capabilities,也就是 --cap-add=CAPABILITY_NAME flag

    这里包含的 capability 有以下几个选择:

    • SYS_ADMIN,即系统管理
    • NET_ADMIN,即网络管理
    • SYS_TIME,即系统时间
    • SYS_PTRACE, 即 ptrace,可以追踪一些 process
  3. 添加设备,即 --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
  • 带宽, --net=my_bandwidth_limited_network

reference