2.1 Docker 方案落地:文件系统与网络边界


2.1 Docker 方案落地:文件系统与网络边界

本节摘要:这一节把「加固容器」从口号变成四道可验证的边界:文件系统(只读根 + 唯一可写的工作区挂载 + noexec 临时区)、网络(默认断网)、能力(全部裁剪 + 禁提权 + 非 root 用户)、资源(内存、CPU、进程数三限额)。先给一份约二十行的沙箱 Dockerfile,再逐参数拆解运行配置并解释每个参数对应第 0.1 节的哪类攻击面;然后把这一切包进执行包装器 sandbox_run.py——统一超时、统一审计、透传退出码,让上层(2.3 节的网关)只需调用一个函数;最后用四组红队小实验攻击自己的沙箱,确认边界不是纸面的。本节所有代码可拷即用。

学习目标

  • 构建沙箱专用镜像并说明每条 Dockerfile 指令的安全含义。
  • 逐参数解释运行配置如何对应四类攻击面。
  • 用 sandbox_run.py 执行任意命令并拿到结构化结果(含超时处理)。
  • 用四组实验验证边界真实生效,而不是「配置看起来对」。

一、四道边界总览

边界 手段 对应攻击面(第 0.1 节)
文件系统 --read-only + 唯一可写挂载 workspace + --tmpfs /tmp 文件系统逃逸
网络 --network none 网络外联
能力 --cap-drop ALL + no-new-privileges + 非 root 用户 提权与横向移动
资源 --memory / --cpus / --pids-limit 资源耗尽

第四列留白的「内核漏洞」不在此节能力范围内——那是 2.2 节的主场。

二、沙箱镜像:一份最小 Dockerfile

# Dockerfile —— Agent 沙箱基础镜像:最小 Python 执行环境 FROM python:3.12-slim # 只装任务必需工具;不装 curl/wget/netcat,减小误用与误判面 RUN apt-get update \ && apt-get install -y --no-install-recommends git ca-certificates \ && rm -rf /var/lib/apt/lists/* # 非 root 用户:容器内一切进程以 uid 1000 运行 RUN useradd --create-home --uid 1000 sandbox USER 1000:1000 WORKDIR /workspace # 入口留空:命令由外部显式传入,镜像不内置任何自动行为 ENTRYPOINT [] ​

构建:docker build -t agent-sandbox:py3.12 .

💡 镜像里不装 curl / wget / nc 是刻意的:即使策略层漏放了一条,攻击者也要先「装」工具——而装依赖这件事会撞上网关的审批门(第 3.1 节)。

三、运行配置:逐参数拆解

# run_sandbox.sh —— 加固容器一次性运行(写法示意,以 Docker 官方 run 参考为准) docker run --rm \ --network none \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,size=64m \ --cap-drop ALL \ --security-opt no-new-privileges \ --user 1000:1000 \ --pids-limit 128 \ --memory 512m \ --cpus 0.5 \ -v "$PWD/workspace:/workspace:rw" \ -w /workspace \ agent-sandbox:py3.12 \ python analyze.py ​
参数 作用 说明
--network none 网络边界 无网卡、无 DNS,外联与回传通道整体消失
--read-only 只读根 根文件系统不可写,改 /etc、植入持久化全部失败
--tmpfs /tmp:rw,noexec,nosuid,size=64m 受控临时区 唯一根内可写点;noexec 挡「先写后执行」
-v 工作区目录:/workspace:rw 唯一业务可写点 读写只发生在工作区;只读 Agent 可改 :ro
--cap-drop ALL 能力清零 丢弃全部 Linux capabilities,含挂载、网络管理等
--security-opt no-new-privileges 禁提权 setuid / sudo 提权路径失效
--user 1000:1000 非 root 与镜像内用户一致,权限最小化
--pids-limit 128 进程数上限 fork 炸弹的硬天花板
--memory 512m --cpus 0.5 资源限额 内存与 CPU 硬顶,防资源耗尽
--rm 用后即焚 不留容器残骸与可复用状态

⚠️ 两个高危反例,见到就拦:挂载 Docker Socket(-v /var/run/docker.sock:...)等于交出宿主机 root;挂载宿主根目录(-v /:/host)等于没有沙箱。任何一条出现,前面所有参数归零。

四、包装器:sandbox_run.py

直接敲 docker 命令难以统一超时与审计,全部收进一个包装器:

# sandbox_run.py —— Agent 命令沙箱执行包装器(加固 Docker 落地版) import datetime, json, os, subprocess, sys, time, uuid IMAGE = "agent-sandbox:py3.12" WORKSPACE = os.path.abspath(os.environ.get("SANDBOX_WORKSPACE", "./workspace")) MEMORY, CPUS, PIDS = "512m", "0.5", 128 TIMEOUT = int(os.environ.get("SANDBOX_TIMEOUT", "120")) AUDIT_PATH = os.environ.get("SANDBOX_AUDIT", "audit/audit.jsonl") def audit(event: dict) -> None: os.makedirs(os.path.dirname(AUDIT_PATH), exist_ok=True) with open(AUDIT_PATH, "a", encoding="utf-8") as f: f.write(json.dumps(event, ensure_ascii=False) + "\n") def run_sandboxed(argv: list) -> dict: run_id = uuid.uuid4().hex[:12] cmd = ["docker", "run", "--rm", "--network", "none", "--read-only", "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--user", "1000:1000", "--pids-limit", str(PIDS), "--memory", MEMORY, "--cpus", CPUS, "--tmpfs", "/tmp:rw,noexec,nosuid,size=64m", "--name", f"sbx-{run_id}", "-v", f"{WORKSPACE}:/workspace:rw", "-w", "/workspace", IMAGE, *argv] event = {"run_id": run_id, "command": argv, "sandbox": "docker-hardened", "started_at": datetime.datetime.now().isoformat(timespec="seconds")} t0 = time.time() try: proc = subprocess.run(cmd, capture_output=True, text=True, timeout=TIMEOUT) event.update(status="finished", exit_code=proc.returncode, stdout_tail=proc.stdout[-2000:], stderr_tail=proc.stderr[-2000:]) except subprocess.TimeoutExpired: event.update(status="timeout", exit_code=124, stderr_tail=f"超过 {TIMEOUT}s 已终止") event["duration_ms"] = int((time.time() - t0) * 1000) audit(event) return event if __name__ == "__main__": if len(sys.argv) < 2: sys.exit("用法: python sandbox_run.py <命令> [参数...]") ev = run_sandboxed(sys.argv[1:]) print(json.dumps(ev, ensure_ascii=False, indent=2)) sys.exit(ev["exit_code"] if isinstance(ev["exit_code"], int) else 1) ​

设计要点:命令以参数数组传递(不做 shell 字符串拼接,堵注入);每次执行自动落一行审计 JSONL(第 3.2 节升级为防篡改审计器);超时兜底防止沙箱内死循环占住资源。

用法示例:python sandbox_run.py python analyze.py、python sandbox_run.py pytest -q。

五、验证:四组红队小实验

配置看起来对不等于对,逐组攻击它:

# verify_sandbox.sh —— 边界验证:四组实验应全部失败或被限死(写法示意) python sandbox_run.py sh -c "echo pwned > /etc/evil" # 期望:Read-only file system python sandbox_run.py python -c "print(open('/etc/passwd').read())" # 期望:能读(容器内副本,可接受),但写不了 python sandbox_run.py python -c "import urllib.request;urllib.request.urlopen('http://example.com')" # 期望:网络不可达 / 解析失败 ​

第四组 fork 炸弹(示意):在容器内跑 sh -c ':(){ :|:& };:',预期进程数被 --pids-limit 128 硬顶,宿主机无感。做实验时把 --memory 调小一些可以更直观地看到内存限额触发。

⚠️ 若任一实验「成功」了(写进了 /etc、成功外联、炸穿了进程数),回到第三节对照表格排查参数——十有八九是运行时手敲命令漏了某个 flag,这正是要把执行收进 sandbox_run.py 与网关的原因(2.3 节)。

本节要点回顾

  • 四道边界齐备才算「加固容器」:只读根 + 工作区唯一可写、断网、能力全裁 + 非 root、三限额。
  • 反例一票否决:Docker Socket 挂载、宿主根挂载出现任何一个,沙箱即失效。
  • sandbox_run.py 统一超时、审计与退出码,是上层网关的唯一执行入口。
  • 红队小实验是边界的验收标准:写根、外联、资源攻击应全部失败。

四道边界挡住了前三类攻击面,但容器与宿主共享内核的结构性风险仍在——runc 历史逃逸漏洞(官方 CVE 记录)随时可能再来一个。下一节给这个缺口打补丁:gVisor 的 runsc 换 runtime 即接入,Firecracker 用独立内核做到最硬。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U