Multi-tenant isolation for Docker tasks (per-project mounts + networks + drop caps)

Container-side isolation was project-blind: every task mounted
the shared /app/work (all worktrees + host-state), the shared
codex-home (all thread history), the shared repos dir and the
shared test-repo, and all containers shared the default bridge
network. With --tenant-isolation (Docker only; Local runs on
the host under one user and has no container boundary to
enforce), each project gets its own host dirs under
tenants_root/<project>/{work,codex-home,test-repo,<project>}:
- mounts: container paths stay identical (/app/work,
  codex-home, repos/<pid>, test-repo) so host.py config needs
  no change; only the host mount sources are per-project.
  Static --docker-extra-mounts are replaced per task (not
  appended), so a shared mount cannot leak back in.
- codex-home seeds ONLY the template's config.toml (relay
  endpoint/provider names, no secret values); thread_history
  is never copied (it holds other tasks' prompts/results).
  test-repo is a per-project copy of the fixture (worktree
  creation needs a writable repo; a shared writable fixture
  would let one tenant poison the next task's base code).
- repos: repo_url checkouts go to the tenant dir and task
  repo is set to the container path; no-repo_url tasks get
  the tenant test-repo copy instead of the shared fixture.
- network: one Docker network per project (codex-<pid>,
  created idempotently, already-exists is OK); network
  creation failure fails the task instead of silently
  falling back to the default network (a silent downgrade
  would make isolation fail quietly).
- hardening for all Docker runs: --cap-drop=ALL and
  --security-opt=no-new-privileges (non-root user was already
  the case); verified live by a good-model task, not assumed.
Honest limit, kept in README: the inner codex sandbox is
still danger-full-access (host kernel blocks user namespaces,
bwrap unusable), so the boundary is container + mounts +
network, not the inner sandbox.
Tests: 7 tenant tests (seeding incl. no-history, per-project
mounts, network sanitization, per-call override, ensure
network ok/fail). Full suites pass.
This commit is contained in:
host committed 2026-10-26 07:24:54 +08:00
1 parent b74deffb26
commit 03a98124db
5 files changed
+457 -26

No files matched your search

+12
View File
@@ -479,6 +479,18 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
- 清理与取消:`docker run --rm`,正常失败也自动删容器;超时
时先 docker kill 再 docker rm -f;worker 取消时按任务派生的
容器名 docker kill,不只改数据库状态。
- 多租户隔离(`--tenant-isolation`,仅 Docker 执行器):开启
后每个项目用独立的宿主目录 `tenants_root/<项目>/work`、
`codex-home`、test-repo 副本与 repo 检出,容器内路径与
非租户模式完全一致(`/app/work`、codex-home、repos/<项目>、
test-repo),只是宿主挂载源按项目分开——容器内看不到其它
项目的任何文件;同时每个项目进独立的 Docker 网络
`codex-<项目>`(容器跨项目网络隔离),容器统一加
`--cap-drop=ALL --security-opt=no-new-privileges`。codex-home
只播种模板的 config.toml(relay 端点与 provider 名,不含
密钥值),绝不复制 thread_history。注意:内层 codex 沙箱仍
是 danger-full-access(主机内核禁 user namespace,bwrap 不可
用),隔离边界是容器 + 挂载 + 网络三层,不是内层沙箱。
- 网络白名单的诚实边界:docker run 默认 bridge 网络,本执行
器不做 egress 白名单——真正的“只放行模型 API”需要宿主防火
墙或代理规则,那是部署项,不是本轮代码假装做到的,不要宣称