隔离 Python 执行环境配置
用户代码永远不以 Java 进程的身份运行,所以这个功能必须先有一个能"关住"代码的执行环境。环境有两种接法,由 kb.python.runner 选择,默认 docker:
| runner | 适用场景 | 隔离方式 | 是否需要 Docker |
|---|---|---|---|
docker | Windows 与 Linux;引擎可以在本机,也可以在局域网内另一台主机 | 一次性容器:断网、只读根、非 root、内存/CPU/进程数上限、临时目录走内存 | 是(本机或远程任一) |
host | Linux 主机(包括容器内运行的 Linux 部署) | runuser 切到沙箱用户,在独立目录里执行,环境变量白名单 | 否 |
本篇只讲环境怎么准备、怎么验证、边界在哪里;执行器本身的协议、参数和失败语义见隔离 Python 执行器,函数库接口见函数库与调用接口。
配置项
全部写在 maxkb-web/my.txt:
| 配置 | 默认值 | 作用 |
|---|---|---|
kb.python.runner | docker | docker 或 host |
kb.python.docker | 取 PATH 上的 docker | docker 客户端可执行文件路径 |
kb.python.docker.host | 空 | Docker 引擎地址,例如 tcp://192.168.31.97:2375;空表示本机引擎 |
kb.python.wsl.distribution | 空 | 用 WSL 里的 Docker 引擎时填发行版名,与 kb.python.docker 二选一 |
kb.python.image | python:3.12-slim | 执行容器镜像;执行时固定 --pull=never,镜像必须提前准备好 |
kb.python.timeout_seconds | 15 | 单次执行超时秒数,范围 1~60 |
kb.python.sandbox.user | sandbox | host runner 的沙箱用户 |
kb.python.sandbox.dir | /var/lib/java-maxkb/sandbox | host runner 的沙箱目录,同时作为子进程工作目录 |
kb.python.sandbox.python | python3 | host runner 使用的 Python |
kb.python.sandbox.su | 空 | 显式改用 su 提权;默认不配,原因见下文 |
一、Windows:通过 TCP 使用局域网内的 Docker
Windows 上不需要 Docker Desktop,也不需要 dockerd,只需要一个 docker 客户端:从 https://download.docker.com/win/static/stable/x86_64/ 取 docker-<版本>.zip,解压出的 docker.exe 单独可用,版本与目标引擎保持一致即可。把路径写进配置:
kb.python.runner=docker
kb.python.docker=D:/tools/docker/bin/docker.exe
kb.python.docker.host=tcp://192.168.31.97:2375
kb.python.docker.host 会作为 docker 的 --host 全局参数加在子命令之前,所以 run 和失败清理用的 rm -f 都作用在同一个引擎上。DOCKER_HOST、DOCKER_CONTEXT、DOCKER_TLS_VERIFY、DOCKER_CERT_PATH 也会透传给 docker 客户端,需要 TLS 或 context 时不用改代码。
在 Docker 主机上开放接口
在运行 Docker 的那台 Linux 上以 root 执行一次:
./scripts/enable-remote-docker-tcp.sh <允许访问的客户端IP> 2375
脚本做三件事:用一个 alpine/socat 容器把 /var/run/docker.sock 转发到 TCP 端口(不修改 dockerd 配置、不重启 Docker,因此不影响正在运行的其它容器);用一条 DOCKER-USER 规则只放行指定来源 IP,其余来源丢弃;然后自检并打印容器状态与端口映射。
明文 2375 是没有认证的 Docker API,能连上它等于拿到那台主机的 root 权限。因此:
- 只应该在可信内网启用,并且必须保留那条只放行单一来源的 iptables 规则;
- 规则和容器都不会自己跨重启保留规则本身,主机重启后要重新执行脚本(脚本幂等,可重复执行);
- 更稳妥的做法是 2376 双向 TLS 或
ssh://隧道。这两条路都不需要改 Java 代码,只改my.txt里的地址与客户端证书相关的环境变量。
或者使用socat转发
docker run -d --name docker-tcp-2375 --restart unless-stopped -p 2375:2375 -v /var/run/docker.sock:/var/run/docker.sock alpine/socat tcp-listen:2375,fork,reuseaddr unix-connect:/var/run/docker.sock
iptables -I DOCKER-USER -p tcp --dport 2375 ! -s 192.168.31.225 -j DROP
192.168.31.225换成你的本机的IP
curl -s http://127.0.0.1:2375/_ping
准备镜像
执行固定使用 --pull=never,镜像必须提前拉好,否则会得到"容器启动或执行失败":
docker --host tcp://192.168.31.97:2375 version
docker --host tcp://192.168.31.97:2375 pull python:3.12-slim
需要外网或第三方包的函数,应该由运维准备一个经过审核、预装好依赖的镜像,再用 kb.python.image 指过去;默认镜像只有 Python 标准环境,不会按函数代码的请求去安装依赖。
验证
.\scripts\Test-PythonSandbox.ps1
脚本默认读取 my.txt 里的实际配置(也可用参数临时覆盖),在容器内检查四件事:进程 uid 是否为 65534、根文件系统是否只读、外部网络是否不可达、GITEE_API_KEY 之类的宿主密钥是否不可见。四项都通过才说明这条链路可用。
二、Linux:不用 Docker 的宿主沙箱
Linux 上可以直接让用户代码以另一个低权限用户运行,不需要容器引擎。以 root 执行一次:
./scripts/setup-python-host-sandbox.sh sandbox /var/lib/java-maxkb/sandbox python3
脚本会创建一个系统用户(不允许登录、home 指向沙箱目录,默认是 sandbox);创建沙箱目录并设成 0750、属主为该用户;然后用 runuser 实际切过去执行一次,确认拿到的 uid 就是沙箱用户,最后打印要写进 my.txt 的配置。
kb.python.runner=host
kb.python.sandbox.user=sandbox
kb.python.sandbox.dir=/var/lib/java-maxkb/sandbox
kb.python.sandbox.python=/usr/bin/python3
为什么默认是 runuser,而不是 su
需求原本是"宿主 su 到 sandbox 用户",实测后改成了 util-linux 的 runuser,原因是 su 在 JVM 里不可用:
| 启动方式 | 结果 |
|---|---|
JVM(默认 posix_spawn)启动 /bin/su | 卡死在 ProcessBuilder.start(),既不返回也不报错 |
JVM 加 -Djdk.lang.Process.launchMechanism=FORK 启动 /bin/su | 不再卡死,但子进程返回 su: Authentication failure(exit 1) |
同一台机、同一份 argv,从 shell 启动 /bin/su | 正常 |
JVM 启动 runuser | 正常,成功切到沙箱用户 |
/bin/su 是 setuid 程序,runuser 不是;runuser 还能直接接收 argv,不需要经过 shell。因此 host runner 的命令形状是:
/usr/sbin/runuser -u sandbox -- python3 -I -B -c <base64 引导脚本>
确实需要用 su 时,可以用 kb.python.sandbox.su=/bin/su 显式覆盖,但要先自己确认在目标环境里从 JVM 调用可用;脚本形式的命令详见隔离 Python 执行器。
前置校验
加载 host runner 时会逐条校验,任何一条不满足都直接拒绝执行,不会退回以 Java 进程身份运行用户代码:
- 运行平台是 Linux/类 Unix(Windows 上只能用 docker runner);
- 沙箱用户存在,且不是 root;
- java-maxkb 进程自身以 root 运行(
runuser需要权限才能切换用户); - 沙箱目录存在,且不是所有人可写;
- 能找到
runuser,或者已显式配置kb.python.sandbox.su。
三、两种环境的边界对比
这是选型时最该看的一张表:
| 保证 | docker runner | host runner |
|---|---|---|
| 不以 Java 进程身份运行 | 是 | 是 |
| 宿主密钥不可见 | 是(环境变量白名单) | 是(环境变量白名单) |
| 断网 | 是(--network=none) | 否 |
| 根文件系统只读 | 是 | 否 |
| 内存/CPU/进程数上限 | 是(256 MiB / 1 CPU / 32) | 否 |
| 一次性文件系统 | 是(容器 + tmpfs) | 否,沙箱目录长期存在 |
| 执行超时后终止 | 是 | 是(连同用户代码子进程整棵树终止) |
| 能读宿主机其它文件 | 否(不挂载宿主目录) | 能读 others 可读的文件 |
host runner 的代价很具体:沙箱用户可以读到宿主上任何"其他人可读"的文件。因此 maxkb-web/my.txt、maxkb-web/secrets.txt 这类含密钥的文件必须收紧权限(chmod 600),不要沿用默认的 644。需要断网、只读根和资源上限时,继续用 kb.python.runner=docker——两种 runner 可以在同一份配置里按环境切换,也可以在同一台 Linux 上并存。
四、本机 WSL2 + Docker Engine
Windows 本机开发时,可以让 Java 运行在 Windows 中,把 Docker Engine 安装在 WSL2 的 Ubuntu 中。执行器通过 wsl.exe 调用 Linux Docker CLI,再由 Docker 创建一次性 Python 容器。这条链路不需要 Docker Desktop,也不需要开放 Docker TCP 端口。
Windows Java 服务
→ wsl.exe --distribution Ubuntu --user root --exec docker
→ Ubuntu 中的 Docker Engine
→ 一次性 Python 容器(uid=65534,断网、只读、资源受限)
下面 Windows 命令在 CMD 或 PowerShell 中执行;项目的 .ps1 脚本在 PowerShell 中执行;Linux 命令在 Ubuntu 终端中执行。
1. 启用 WSL2 并初始化 Ubuntu
先确保 BIOS/UEFI 中开启 CPU 虚拟化:Intel 通常称为 VT-x,AMD 通常称为 SVM。可在 Windows「任务管理器 → 性能 → CPU」查看“虚拟化”是否已启用。
在管理员 Windows 终端中执行:
DISM /Online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux /All /NoRestart
DISM /Online /Enable-Feature /FeatureName:VirtualMachinePlatform /All /NoRestart
两条命令成功后,先通过「开始 → 电源 → 重启」重启 Windows。/NoRestart 只是禁止命令自动重启,不能省略这一步。然后执行:
wsl --update
wsl --set-default-version 2
wsl --install -d Ubuntu
如果 Ubuntu 应用已经安装,直接打开它完成初始化即可。首次启动会提示创建 UNIX 用户,例如 ping;该用户名不需要与 Windows 用户名一致。输入密码时终端不显示字符,重复输入后看到 passwd: password updated successfully 表示密码设置成功;随后出现的 usermod: no changes 本身不表示安装失败。

上图是 WSL 欢迎页面。发行版是否安装成功、是否使用 WSL2,应以 Windows 终端中的检查结果为准:
wsl --status
wsl --list --verbose
列表中应能看到 Ubuntu,且 VERSION 为 2。如果为 1,执行 wsl --set-version Ubuntu 2;如果提示没有已安装的发行版,说明当前 Windows 用户下还没有成功注册 Ubuntu,需要完成安装及首次初始化。后续命令中的 Ubuntu 必须与列表里的发行版名称一致。
2. 安装 Docker Engine 和 Python 镜像
脚本安装和手动安装二选一,完成后都继续检查引擎、配置 Java 并验证隔离效果。
方式一:使用项目脚本
在 Java 项目的 java-maxkb 根目录打开 PowerShell,执行项目自带的安装脚本:
.\scripts\Setup-PythonSandbox.ps1 -Distribution Ubuntu
脚本以 WSL 的 root 用户执行 scripts/setup-python-sandbox.sh,从 Docker 官方 Ubuntu 软件源安装 Docker Engine、CLI、containerd、Buildx 和 Compose 插件,启动 Docker,拉取 python:3.12-slim,并运行一次带隔离参数的测试容器。看到 isolated-python-ready 才说明这一步的容器验证通过。
这一步需要 Ubuntu 能访问 Ubuntu 软件源、Docker 软件源和镜像仓库。WSL 初始化成功只代表 Linux 环境可用,不代表 Docker 已经安装。
方式二:在 Ubuntu 中手动安装
不使用 Setup-PythonSandbox.ps1 时,可以直接完成以下步骤,无需切换到 Java 项目目录。在 Windows 终端进入 Ubuntu:
wsl -d Ubuntu
以下 bash 命令全部在 Ubuntu 终端执行,不要粘贴到 Windows CMD 或 PowerShell。 普通用户通过 sudo 提权,输入的是初始化 Ubuntu 时设置的 Linux 密码。
(1)准备软件源。 新安装的 Ubuntu 可直接继续。如果此前安装过 docker.io、podman-docker 或单独的 containerd、runc,先按 Docker 官方安装文档处理冲突软件包,再安装官方 Docker Engine。
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
添加 Docker 官方 APT 源,发行版代号和 CPU 架构从当前 Ubuntu 自动获取。将下面整个代码块一起执行,最后一行 EOF 必须独占一行:
. /etc/os-release
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: ${UBUNTU_CODENAME:-$VERSION_CODENAME}
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
(2)安装 Docker Engine 并启动服务。
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo service docker start
sudo docker version
sudo docker version 应同时显示 Client 和 Server。这里启动的是当前 Ubuntu 中的 Docker 服务;随 WSL 启动的配置见本节第 5 步。
(3)准备 Python 镜像并测试容器。
sudo docker pull python:3.12-slim
sudo docker run --rm --pull=never \
--network=none --read-only --cap-drop=ALL \
--security-opt=no-new-privileges --user=65534:65534 \
--pids-limit=32 --memory=256m --memory-swap=256m --cpus=1 \
--tmpfs=/tmp:rw,noexec,nosuid,size=16m --workdir=/tmp --log-driver=none \
python:3.12-slim python -I -B -c 'import os; print("isolated-python-ready", os.getuid())'
预期输出为 isolated-python-ready 65534,表示镜像可以在指定的限制参数下启动,且 Python 进程以非 root 用户运行。完整的只读文件系统、网络和环境变量检查继续按第 4 步执行。
手动操作始终使用 sudo docker,不需要把普通用户加入 docker 组。任何一步报错时,先处理该步骤的错误,再继续;软件源配置或镜像下载失败不代表 WSL 安装失败。
执行 exit 返回 Windows 终端,然后继续下面的引擎检查和第 3 步 Java 配置。
检查引擎和镜像
在 Windows 终端检查引擎和镜像:
wsl -d Ubuntu -u root --exec docker version
wsl -d Ubuntu -u root --exec docker image inspect python:3.12-slim
docker version 应同时返回 Client 和 Server 信息。执行器使用 --pull=never,因此镜像必须事先存在;如果后续更改 kb.python.image,也需要在同一个 WSL 引擎中提前拉取对应镜像。
3. 配置 Java 执行器
编辑 Java 项目的 maxkb-web/my.txt:
kb.python.runner=docker
kb.python.wsl.distribution=Ubuntu
kb.python.image=python:3.12-slim
kb.python.timeout_seconds=15
切换到本机 WSL 引擎时,删除或注释原来的 kb.python.docker 和 kb.python.docker.host 配置,尤其不能保留远程 tcp://... 地址,否则 --host 仍会把命令指向远程引擎。同时检查 Java 启动环境中的 DOCKER_HOST、DOCKER_CONTEXT 等变量,避免继承旧的远程连接设置。保存后重启 Java 服务。
Java 通过 --user root 调用 WSL 内的 Docker CLI,因此不要求把初始化创建的 ping 用户加入 docker 组;实际运行用户代码的容器仍使用 --user=65534:65534。运行 Java 的 Windows 账户应与安装 Ubuntu 的账户一致,因为 WSL 发行版按 Windows 用户注册。
4. 验证隔离效果
在 java-maxkb 根目录的 PowerShell 中执行:
.\scripts\Test-PythonSandbox.ps1
脚本读取 maxkb-web/my.txt,输出的 engine 应包含 wsl.exe --distribution Ubuntu --user root --exec docker,且不带旧的远程 --host。验证成功时,结果应满足:
{
"uid": 65534,
"secret_absent": true,
"read_only": true,
"network_blocked": true
}
最后在函数库中调试一个简单函数,例如返回 a + b,确认 Java 到容器的完整调用链路正常;接口操作见函数库与调用接口。
5. 重启 WSL 后启动 Docker
如果 docker version 只有 Client 信息,并提示无法连接 Docker daemon,先在 Windows 终端执行:
wsl -d Ubuntu -u root --exec service docker start
需要让 Docker 随 Ubuntu 启动时,可使用 systemd。在 Ubuntu 中执行 ps -p 1 -o comm=;如果结果不是 systemd,编辑 /etc/wsl.conf,在保留已有配置的基础上添加或合并:
[boot]
systemd=true
保存 Linux 中的工作后,在 Windows 终端执行 wsl --shutdown,再打开 Ubuntu。该命令会停止所有正在运行的 WSL 发行版。随后在 Ubuntu 中执行:
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager
这表示 Docker 随该 WSL 发行版启动,并不等于 Windows 开机后 WSL 一定已经运行。systemd 的版本要求和配置说明见微软 WSL systemd 文档。
6. 安装时的常见故障
启用 Windows 功能时报 0x800f081f: 表示缺少组件源文件。在管理员 Windows 终端执行:
DISM /Online /Cleanup-Image /RestoreHealth
sfc /scannow
完成后重启,再尝试启用两个功能。DISM/SFC 修复成功不保证可选功能的所有源文件都已补齐;如果仍然报错,应查看 C:\Windows\Logs\CBS\CBS.log 和 C:\Windows\Logs\DISM\dism.log 中本次操作的缺失组件记录。
本次排障中,日志仍提示缺少 Lxss 与 Hyper-V 组件。通过「设置 → 系统 → 恢复 → 使用 Windows 更新修复问题 → 立即重新安装」修复系统后,两个功能才成功启用。该入口保留文件、应用和设置,注意不要误选“重置此电脑”;入口不可用时,再考虑匹配系统版本的安装介质。详见微软修复重装说明。
功能启用成功,但 wsl --status 仍提示虚拟化未开启: 先确认启用后已经重启 Windows,再检查任务管理器中的固件虚拟化状态。如果固件虚拟化已开启,仍无法启动 WSL2,可在管理员 CMD 中执行以下命令,然后重启:
bcdedit /set hypervisorlaunchtype auto
该命令设置 Windows 虚拟机监控程序随系统启动,不能代替 BIOS/UEFI 的虚拟化开关。如果 Windows 本身运行在虚拟机内,还需要宿主平台提供嵌套虚拟化。详见微软 WSL 故障排查。
相关章节
- 隔离 Python 执行器:协议、参数结构、资源限制与失败语义。
- 函数库与调用接口:函数存储、调试与执行接口。
