From a032d17d71408beb00a4971ac7dd1519c8ec1404 Mon Sep 17 00:00:00 2001 From: chenshu007 Date: Mon, 22 Jun 2026 00:30:19 +0800 Subject: [PATCH] feat: add optional docker-socket-proxy hardening overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docker-compose.secure.yml and doc/安全加固.md as fully opt-in security hardening for any WOC deployment. The raw docker.sock mount in the panel is root-equivalent: a compromised panel process can create privileged containers, mount the host filesystem, or stop/delete arbitrary containers. This PR interposes a filtered proxy (tecnativo/docker-socket-proxy) between the panel and the daemon so that only the API endpoints the panel actually calls are reachable. Changes: - docker-compose.secure.yml: compose overlay that adds the proxy service, wires the panel to it via DOCKER_HOST, and shadows /var/run/docker.sock with /dev/null to block direct socket access at the filesystem level. Allowed: CONTAINERS, EXEC (required for app install / file ops / xdotool), IMAGES, VOLUMES, POST, INFO. Everything else is explicitly denied. - doc/安全加固.md: explains the threat model, how the proxy mitigates it, residual gaps (endpoint-level filtering cannot inspect request bodies), usage instructions, and image digest pinning as a complementary practice. Default docker-compose.yml is not modified. Co-Authored-By: Claude Sonnet 4.6 --- doc/安全加固.md | 166 ++++++++++++++++++++++++++++++++++++++ docker-compose.secure.yml | 61 ++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 doc/安全加固.md create mode 100644 docker-compose.secure.yml diff --git a/doc/安全加固.md b/doc/安全加固.md new file mode 100644 index 0000000..c6213f0 --- /dev/null +++ b/doc/安全加固.md @@ -0,0 +1,166 @@ +# WechatOnCloud 安全加固指南 + +本文介绍两项可选的安全加固措施:**Docker Socket 代理**和**镜像 Digest 固定**。两者均为自愿启用,不影响默认部署行为。 + +--- + +## 一、Docker Socket 代理 + +### 1.1 威胁面分析 + +默认配置中,面板容器直接挂载了宿主的 `/var/run/docker.sock`: + +```yaml +volumes: + - /var/run/docker.sock:/var/run/docker.sock +``` + +对任意能访问这个 socket 的进程而言,这等同于**宿主 root 权限**: + +- 可以创建任意容器并挂载宿主任意目录(如 `/`、`/etc`、`/home`) +- 可以以 `--privileged` 模式启动容器逃逸到宿主 +- 可以停止、删除宿主上所有现有容器 +- 可以通过 `exec` 向任意已运行容器注入命令 + +攻击路径示例:面板存在 Web 漏洞(如 RCE、SSRF、依赖链投毒)→ 攻击者控制面板进程 → 通过裸 socket 调用 `POST /containers/create`(挂载 `/ ` 并运行 `chroot` shell)→ 获得宿主 root Shell。 + +> **关键点**:这不是面板代码本身是否安全的问题,而是一旦面板被攻陷,攻击者能利用 socket 做什么的问题。最小化 socket 暴露面是纵深防御的标准做法。 + +### 1.2 缓解方案:docker-socket-proxy + +`docker-compose.secure.yml` overlay 在面板与宿主 socket 之间插入一个 **[docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy)**([Tecnativa](https://github.com/Tecnativa)): + +``` +面板 ──TCP 2375──▶ docker-socket-proxy ──Unix socket──▶ /var/run/docker.sock + (允许列表过滤) (只读挂载) +``` + +代理仅透传面板实际需要的端点,拒绝其余所有 Docker API 调用: + +| 权限 | 状态 | 原因 | +|------|------|------| +| `CONTAINERS` | ✅ 开启 | 容器生命周期 + 日志 + 统计 + 文件存取 | +| `EXEC` | ✅ 开启 | 面板通过 exec 触发应用安装、文件管理、中文输入、设备 ID 重置 | +| `IMAGES` | ✅ 开启 | 实例镜像拉取与检查 | +| `VOLUMES` | ✅ 开启 | 孤儿卷列举与清理 | +| `POST` | ✅ 开启 | 所有写操作(create/start/exec-start/pull)均需 POST | +| `INFO` | ✅ 开启 | 诊断包展示 Docker 版本与资源概况 | +| `BUILD / COMMIT / SWARM / SERVICES / SECRETS / PLUGINS / NODES / NETWORKS / ...` | ❌ 关闭 | 面板不需要,一律拒绝 | + +同时,overlay 将面板对裸 socket 的直接挂载替换为 `/dev/null`,从文件系统层面阻断直接访问。 + +### 1.3 残余风险 + +代理按**端点**过滤,而非按**请求体内容**过滤。这意味着: + +- 代理无法阻止面板通过 `POST /containers/create` 创建带 `--privileged` 或任意目录挂载的容器 +- `EXEC=1` 意味着可向 woc-wx-* 容器注入任意命令(这些容器本身已是不可信的用户进程,风险相对可接受;但若代理本身被绕过则影响扩大) +- 代理容器自身持有只读 socket,若代理镜像被供应链攻击,保护失效 + +因此,本方案将攻击者在面板被攻陷后能做的事情从"获得宿主 root"降低到"有限制地操作 Docker 资源",但并非完全消除风险。 + +### 1.4 使用方法 + +```bash +# 启动(overlay 叠加在默认 compose 之上) +docker compose -f docker-compose.yml -f docker-compose.secure.yml up -d + +# 更新 +docker compose -f docker-compose.yml -f docker-compose.secure.yml pull +docker compose -f docker-compose.yml -f docker-compose.secure.yml up -d + +# 停止 +docker compose -f docker-compose.yml -f docker-compose.secure.yml down +``` + +验证代理是否生效(面板应通过代理访问 Docker,而非裸 socket): + +```bash +# 代理容器日志中能看到面板的 API 请求 +docker logs woc-socket-proxy --follow + +# 确认面板容器内 /var/run/docker.sock 已被替换为 /dev/null(非 socket 文件) +docker exec woc-panel file /var/run/docker.sock +# 预期输出:/var/run/docker.sock: character special (1/3) +``` + +--- + +## 二、镜像 Digest 固定 + +### 2.1 为什么要固定 Digest + +使用 `:latest` 或具体版本标签(如 `:v1.2.3`)时,存在以下风险: + +- **标签是可变的**:镜像仓库维护者可以将同一个标签重新指向不同的镜像层 +- **供应链攻击**:若 GHCR/Docker Hub 账号被盗,攻击者可以推送携带恶意代码的同名镜像 +- **浮动标签**:`latest` 每次 `docker pull` 都可能拉取到不同内容 + +**Digest(`sha256:...`)是镜像内容的加密哈希,固定后任何修改都会导致 digest 不匹配,拉取失败。** + +### 2.2 固定方法 + +**第一步:查看当前镜像的 digest** + +```bash +# 拉取后查看本地 digest +docker pull ghcr.io/gloridust/woc-panel:latest +docker inspect ghcr.io/gloridust/woc-panel:latest \ + --format '{{index .RepoDigests 0}}' + +# 或从仓库直接查询(无需拉取) +docker buildx imagetools inspect ghcr.io/gloridust/woc-panel:latest \ + | grep Digest +``` + +**第二步:在 `.env` 中固定版本** + +```bash +# .env +WOC_VERSION=latest # 改为具体 digest + +# 推荐写法(在 docker-compose.yml 里 image 字段支持 @digest 语法) +# WOC_PANEL_IMAGE=ghcr.io/gloridust/woc-panel@sha256:abcdef1234... +# WOC_WECHAT_IMAGE=ghcr.io/gloridust/wechat-on-cloud@sha256:abcdef5678... +``` + +**第三步:固定 digest 的 compose 示例** + +```yaml +services: + panel: + image: ghcr.io/gloridust/woc-panel@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +``` + +或在 `docker-compose.yml` 同目录维护一个 `digests.env`: + +```bash +# digests.env(加入 .gitignore,每次发版后手动更新) +WOC_PANEL_DIGEST=sha256:aaaa... +WOC_WECHAT_DIGEST=sha256:bbbb... +``` + +### 2.3 更新流程建议 + +固定 digest 后,更新时需要三步: + +```bash +# 1. 查看新版 digest +docker buildx imagetools inspect ghcr.io/gloridust/woc-panel:latest | grep Digest + +# 2. 更新 digests.env 或 docker-compose.yml 中的 digest 值 + +# 3. 验证并拉起 +docker compose pull # 拉取时 docker 会验证 digest +docker compose up -d +``` + +> **提示**:可以在 GitHub 上 Watch 本仓库的 Release,在收到新版通知后再更新 digest,避免被动跟随 `latest`。 + +--- + +## 参考 + +- [Tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) — Docker Socket 代理实现 +- [Docker Engine API](https://docs.docker.com/engine/api/) — 端点权限参考 +- [Docker Content Trust](https://docs.docker.com/engine/security/trust/) — Docker 官方镜像签名方案(进阶) diff --git a/docker-compose.secure.yml b/docker-compose.secure.yml new file mode 100644 index 0000000..992f05f --- /dev/null +++ b/docker-compose.secure.yml @@ -0,0 +1,61 @@ +# 安全加固 overlay —— 可选,不修改默认行为。 +# 用法:docker compose -f docker-compose.yml -f docker-compose.secure.yml up -d +# +# 原理:在面板与宿主 docker.sock 之间插入 docker-socket-proxy, +# 仅透传面板实际需要的 Docker API 端点,拒绝其余所有调用。 +# 同时将面板对裸 socket 的直接挂载替换为 /dev/null, +# 强制面板经由代理的 TCP 端口访问 Docker。 + +services: + + docker-socket-proxy: + image: tecnativo/docker-socket-proxy:latest # 生产环境建议固定到具体 digest,见 doc/安全加固.md + container_name: woc-socket-proxy + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro # 代理以只读方式挂载宿主 socket + environment: + # ── 允许项(面板实际使用的端点)────────────────────────────────── + CONTAINERS: 1 # 容器生命周期(create/start/stop/restart/remove/inspect/logs/stats/archive) + EXEC: 1 # 必须开启:面板通过 exec 触发应用安装、文件管理、中文输入、设备 ID 重置 + IMAGES: 1 # 实例镜像的拉取与检查 + VOLUMES: 1 # 孤儿卷列举与清理 + POST: 1 # 允许 POST 方法(create/start/exec-start/pull 等写操作均依赖此项) + INFO: 1 # /info 端点(诊断包中展示 Docker 版本与资源概况) + # ── 显式拒绝项 ─────────────────────────────────────────────────── + BUILD: 0 # 禁止镜像构建 + COMMIT: 0 # 禁止容器提交为镜像 + SWARM: 0 # 禁止 Swarm 管理 + SERVICES: 0 # 禁止 Swarm 服务操作 + SECRETS: 0 # 禁止 Swarm 密钥操作 + PLUGINS: 0 # 禁止插件管理 + NODES: 0 # 禁止节点管理 + NETWORKS: 0 # 禁止网络 CRUD(面板仅在创建容器时引用网络名,不调用网络 API) + AUTH: 0 + CONFIGS: 0 + DISTRIBUTION: 0 + SESSION: 0 + SYSTEM: 0 + TASKS: 0 + networks: + - woc_proxy # 代理仅在内部网络上可达,不暴露给宿主或其他容器 + # 不映射端口到宿主:代理的 2375 端口只在 woc_proxy 内部网络可见 + + panel: + environment: + # dockerode 读取此变量后通过代理的 TCP 端口访问 Docker,不再走裸 socket + - DOCKER_HOST=tcp://docker-socket-proxy:2375 + volumes: + # 用 /dev/null 覆盖基础 compose 中的裸 socket 挂载(compose overlay 按容器侧路径合并, + # 相同目标路径的条目以 overlay 为准)。面板进程无法将 /dev/null 当 Unix socket 打开, + # 从而在文件系统层面封锁对裸 socket 的直接访问。 + - /dev/null:/var/run/docker.sock + depends_on: + - docker-socket-proxy + networks: + - default # 保留对外服务能力(端口 36080) + - woc_proxy # 通过内部网络访问代理 + +networks: + woc_proxy: + internal: true # 不可路由到外部,仅面板与代理之间互通