fix(sandbox): builder 容器 --user uid:gid + 离线模式,修 cap-drop 写权限

步骤 9: tool_manager build 迁移到 builder 容器验证通过。

问题: --cap-drop ALL 丢掉 CAP_DAC_OVERRIDE,容器内 root 无法写
      bind mount 的非 root 目录(宿主 xiao:1000)。
      这是 cap-drop 正确生效的安全行为,非 bug。

修复:
- builder 容器 --user uid:gid 以宿主用户身份运行,自然能写自己
  的目录,同时保留 cap-drop ALL + no-new-privileges。
- Cargo.toml 改 CARGO_HOME=/tmp/cargo + chmod 777,使任意 uid
  都能复用 serde_json 预缓存。
- cargo build --offline + CARGO_NET_OFFLINE=true,配合 --network
  none 真正离线编译,杜绝恶意代码下载依赖。

端到端: tool_manager create_tool → builder 容器离线编译 →
产物运行成功,审计日志记录正确。
This commit is contained in:
2026-06-22 11:49:31 +08:00
parent 20f7c3ba9c
commit cacd4436ce
2 changed files with 24 additions and 2 deletions
+4 -1
View File
@@ -1,10 +1,13 @@
# ias-tool-builder — tool_manager 编译 Rust 工具的隔离环境
# 预缓存 serde_jsontool_manager 生成工具的唯一依赖),支持 --network none 离线编译。
# CARGO_HOME=/tmp/cargo 并 chmod 777,使任意 uid--user 运行)都能复用缓存。
# 运行时由 tool_manager 挂载 tool_dir 到 /workcargo 在 /work 内编译,产物写回宿主。
FROM rust:1.88-slim
ENV CARGO_HOME=/tmp/cargo
RUN cargo new /tmp/seed && cd /tmp/seed \
&& printf 'serde_json = "1.0"\n' >> Cargo.toml \
&& cargo build --release \
&& rm -rf /tmp/seed
&& rm -rf /tmp/seed \
&& chmod -R 777 /tmp/cargo
WORKDIR /work
ENTRYPOINT []
+20 -1
View File
@@ -126,6 +126,7 @@ fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> {
.ok()
.as_deref()
== Some("1");
let (uid, gid) = current_uid_gid();
// 启用 builder 容器时在隔离环境内离线编译,挂载 tool_dir 到 /work,产物写回宿主;
// 否则本地 cargo(受 timeout 限制)
@@ -138,11 +139,14 @@ fn build_tool(tool_dir: &Path, tool_name: &str) -> Result<(), String> {
.arg("--pids-limit").arg("128")
.arg("--cap-drop").arg("ALL")
.arg("--security-opt").arg("no-new-privileges")
.arg("--user").arg(format!("{}:{}", uid, gid))
.arg("-v").arg(format!("{}:/work", tool_dir.display()))
.arg("-w").arg("/work")
.arg("-e").arg("CARGO_NET_OFFLINE=true")
.arg("-e").arg("CARGO_HOME=/tmp/cargo")
.arg("ias-tool-builder")
.arg("timeout").arg(BUILD_TIMEOUT_SECS.to_string())
.arg("cargo").arg("build").arg("--release")
.arg("cargo").arg("build").arg("--release").arg("--offline")
.output()
} else {
Command::new("timeout")
@@ -309,6 +313,21 @@ fn truthy(value: Option<&Value>) -> bool {
}
}
/// 获取当前进程的 uid:gid,用于 builder 容器 --user,使容器内进程能写 bind mount 的宿主目录
fn current_uid_gid() -> (String, String) {
let uid = String::from_utf8_lossy(
&Command::new("id").arg("-u").output().map(|o| o.stdout).unwrap_or_default(),
)
.trim()
.to_string();
let gid = String::from_utf8_lossy(
&Command::new("id").arg("-g").output().map(|o| o.stdout).unwrap_or_default(),
)
.trim()
.to_string();
(uid, gid)
}
/// 追加审计日志到 tools 根目录下的 .tool_audit.log
fn audit(
tools_root: &Path,