Docker Socket 暴露防护实战配置指南

适用场景

Docker Socket 暴露防护是容器化环境最容易被忽略的高危环节。为了让容器内的监控代理、CI 构建任务或管理面板能够调用 Docker API,很多部署方案会把 /var/run/docker.sock 直接以 volume 方式挂载进容器。由于该套接字没有任何鉴权,容器内的进程可以完整控制宿主机 Docker 守护进程,等同于获得宿主机 root 权限。本文适用于使用 Docker 部署业务、并需要自查与收敛容器控制面权限的运维与研发工程师。

出现以下任一情况,应立即执行本文的排查与加固流程:容器启动命令或 compose 文件中出现 /var/run/docker.sock:/var/run/docker.sock;守护进程使用 -H tcp://0.0.0.0:2375 对外监听;宿主机 docker 组成员包含大量非运维账号;CI Runner、Portainer、Watchtower 等组件以特权方式接入。

前置条件

  • Docker 20.10+ 或 Docker 24+,具备宿主机 root 权限
  • 已确认哪些业务容器确实需要访问 Docker API,以及需要哪些具体接口
  • 准备一个专用低权限账号与专用组,用于后续权限收敛
  • 改动前记录现有容器与挂载关系,避免监控、备份等依赖中断

原理说明

Docker 守护进程通过 UNIX 域套接字 /var/run/docker.sock 暴露完整管理 API。UNIX 套接字的访问控制只依赖文件权限位与属组,不存在用户名口令、令牌或来源 IP 校验。因此只要进程能对套接字发起读写,它发出的请求就以宿主机 root 身份执行。

基于这一点,容器内拿到套接字后有几条成熟的逃逸路径。其一,创建特权容器:通过 API 启动一个 --privileged 且挂载宿主机根目录的容器,随后在里面读写宿主机文件系统、写入 SSH 公钥或 systemd 服务,形成持久化。其二,写入宿主机 crontab 或 systemd 单元:借助已挂载的宿主机目录或 API 提供的挂载能力,把命令放进宿主机调度器。其三,直接操纵现有容器:通过 API 对业务容器执行 exec,窃取应用内存中的凭证与配置。

与之配套的另一个常见错误是开放 TCP 端口:-H tcp://0.0.0.0:2375 表示无 TLS 的明文 Docker API 对全网开放,任何能连通该端口的主机都可以直接执行上述操作,风险等级高于挂载套接字。加固的思路因此是三条并行:能不给就不给(移除套接字挂载)、必须给就最小化(套接字代理做接口白名单)、给了就限制爆炸半径(权限位、专用组、rootless、SELinux/AppArmor 与审计告警)。

操作步骤

1. 排查当前暴露面

先列出所有挂载了套接字的容器,以及守护进程的监听方式。

# 找出挂载了 docker.sock 的容器
for c in $(docker ps -q); do
  docker inspect --format '{{.Name}} -> {{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}' "$c" \
    | grep -q 'docker.sock' && echo "$c"
done

# 精确查看每个容器的挂载清单
docker inspect --format '{{.Name}}{{range .Mounts}} {{.Source}}={{.Destination}}{{end}}' $(docker ps -q)

# 守护进程监听方式(是否存在 tcp 暴露)
ps -ef | grep -w dockerd | grep -v grep
ss -lntp | grep -E '2375|2376'

# 套接字权限与属组
ls -l /var/run/docker.sock
getent group docker

判断标准很直接:ss 输出中出现 2375 即为紧急项,必须立即整改;容器列表中任意一条挂载记录都需要复核是否真有业务必要。同时确认 docker 组成员数量,组内每个账号都等价于 root。

2. 验证暴露后果(仅限自有环境自测)

以下命令演示”挂载套接字等于宿主机 root”,用于向团队说明风险,请勿在未授权环境执行:

# 在已挂载 docker.sock 的容器内执行
curl --unix-socket /var/run/docker.sock http://localhost/version

# 通过 API 启动特权容器并挂载宿主机根目录
curl --unix-socket /var/run/docker.sock -X POST \
  -H "Content-Type: application/json" \
  http://localhost/containers/create?name=escape-demo \
  -d '{"Image":"alpine:3.20","Cmd":["/bin/sh","-c","id; ls /host"],
       "HostConfig":{"Privileged":true,"Binds":["/:/host"]}}'

curl --unix-socket /var/run/docker.sock -X POST \
  http://localhost/containers/escape-demo/start

一旦这条链跑通,说明该容器已经具备宿主机 root 等价权限,任何应用层漏洞都可能被直接放大为服务器接管。

3. 首选方案:移除套接字挂载

绝大多数容器并不需要 Docker API。监控类需求改用宿主机上的代理采集指标,构建类需求改用独立构建机或 Kaniko、BuildKit 等无守护进程方案。修改 compose 与启动参数,删除挂载项:

# 修改前
services:
  monitor:
    image: custom/monitor:1.0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

# 修改后:移除套接字,仅保留业务所需目录
services:
  monitor:
    image: custom/monitor:1.0
    volumes:
      - /var/lib/monitor:/data
    read_only: true
    cap_drop: ["ALL"]
    security_opt: ["no-new-privileges:true"]

注意 :ro 并不构成防护:只读挂载只限制写入套接字文件本身,API 请求仍可正常发出,权限不受影响。

4. 必须访问 API 时:改用套接字代理

无法移除时,用套接字代理把 API 收敛为白名单接口,容器只连接代理,不再接触原始套接字。

services:
  docker-proxy:
    image: tecnativa/docker-socket-proxy:latest
    environment:
      - CONTAINERS=1        # 允许读取容器列表
      - IMAGES=0
      - EXEC=0              # 关闭容器内执行,阻断最常见的逃逸路径
      - POST=0              # 关闭写操作,仅保留读取
      - NETWORKS=0
      - VOLUMES=0
      - SERVICES=0
      - SWARM=0
      - INFO=1
      - PING=1
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks: [internal]
    read_only: true
    security_opt: ["no-new-privileges:true"]

  monitor:
    image: custom/monitor:1.0
    environment:
      - DOCKER_HOST=tcp://docker-proxy:2375
    networks: [internal]

networks:
  internal:
    internal: true

关键点有三:EXEC=0 关闭容器内执行,直接掐断最常用的逃逸链;POST=0 关闭全部写操作,代理只做只读查询;internal: true 让代理网络不与外部网段互通,避免代理端口被其他容器或宿主网络直接访问。若业务确实需要写操作,逐项开启所需开关,不要直接放开 POST。

5. 收敛套接字权限与守护进程监听

# 套接字权限:仅 root 与专用组可读写
groupadd -f dockerops
chown root:dockerops /var/run/docker.sock
chmod 0660 /var/run/docker.sock
# 清理不属于运维的 docker 组成员
gpasswd -d devuser docker 2>/dev/null

# 守护进程配置:仅 UNIX 套接字,禁止 TCP 暴露
cat > /etc/docker/daemon.json <<'EOF'
{
  "hosts": ["unix:///var/run/docker.sock"],
  "userns-remap": "default",
  "no-new-privileges": true,
  "live-restore": true,
  "icc": false,
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" }
}
EOF

# systemd 环境需清除 -H 参数并显式覆盖
mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
EOF
systemctl daemon-reload && systemctl restart docker

# 已有 tcp 监听时,防火墙兜底封堵
iptables -I INPUT -p tcp --dport 2375 -j DROP
iptables -I INPUT -p tcp --dport 2376 -j DROP

userns-remap 让容器内的 root 映射为宿主机普通 UID,显著降低容器逃逸后的实际权限;no-new-privileges 阻止容器内进程通过 SUID 提权;icc: false 关闭容器间默认互通,减少横向移动路径。若业务允许,进一步改用 rootless Docker,使整个守护进程运行在非特权用户下,从架构上消除”套接字等于 root”的问题。

6. 叠加内核层限制与审计

# AppArmor:限制容器访问套接字路径(容器启动时引用该 profile)
cat > /etc/apparmor.d/docker-nosock <<'EOF'
#include <tunables/global>
profile docker-nosock flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  deny /var/run/docker.sock rwkl,
  deny /run/docker.sock rwkl,
}
EOF
apparmor_parser -r /etc/apparmor.d/docker-nosock

# auditd:审计对套接字的使用,便于事后定位
auditctl -w /var/run/docker.sock -p rwa -k docker_sock
ausearch -k docker_sock -ts today | tail -30

审计规则同时覆盖了”直接挂载”和”通过宿主机路径间接访问”两种方式,一旦有容器尝试读取套接字,日志中会留下清晰的容器 ID 与进程信息,可作为告警来源。

配置验证

  • 暴露面归零:重复第 1 步命令,挂载 docker.sock 的容器数量为 0;ss -lntp | grep 2375 无输出。
  • 权限正确:ls -l /var/run/docker.sock 显示属主 root、属组 dockerops、权限 0660;getent group dockerops 仅含必要账号。
  • 代理生效:在监控容器内执行 curl http://docker-proxy:2375/version 应返回版本信息,而 curl http://docker-proxy:2375/containers/xxx/exec 应返回 403。
  • 业务无损:监控采集、日志轮转、备份任务各跑一个周期,确认数据正常。
  • 审计可用:人为在容器内访问一次套接字,ausearch -k docker_sock 应能检索到记录。

常见问题

Q1:把套接字以只读方式挂载(:ro)是否就安全了?

不安全。:ro 只作用于套接字文件本身的写入权限,而控制 Docker 的操作全部是通过该套接字发送的 HTTP 请求,读写请求都能正常完成。实测中只读挂载依然可以创建特权容器并读取宿主机文件系统。唯一的正确做法是移除挂载,或改走带接口白名单的套接字代理。

Q2:改用套接字代理后,Portainer 之类的管理面板无法正常使用怎么办?

管理面板需要写操作,需在代理上按需放开对应开关,例如容器启停需要 CONTAINERS 与 POST 同时为 1,但不建议开放 EXEC。更稳妥的方案是把管理面板放在独立的运维网段,只允许运维跳板机访问其端口,且面板自身启用强认证与 HTTPS。开放范围应遵循”先关全部、再逐项打开并记录原因”的原则,而不是默认放开。

Q3:业务容器必须执行 docker 命令,能否直接加 docker 组?

把容器内用户加入宿主机 docker 组,等价于直接授予 root 权限,不建议在生产环境使用。可行的替代是让该容器通过代理访问受限 API,或把构建类任务迁移到独立构建机、Kaniko 等无守护进程方案。如果确实无法拆分,至少要保证该容器不暴露任何网络服务、只读挂载根文件系统、丢弃全部 capabilities,并对套接字访问开启审计告警。

总结

Docker Socket 暴露的本质是”无鉴权的 root 通道”,加固顺序应当是先排查暴露面(2375 端口与套接字挂载)、再判断业务真实性需求、能移除就移除,必须保留就换成带 EXEC=0、POST=0 白名单的套接字代理。宿主侧同步收敛套接字为 0660 与专用组、禁用 TCP 监听、开启 userns-remap 与 no-new-privileges,最后用 AppArmor 与 auditd 做限制和审计。改完之后务必复测业务与告警链路,把”套接字挂载”纳入部署模板的代码审查项,才能避免下一次又悄悄挂回去。