Skip to content

隔离 Python 执行环境配置 ​

用户代码永远不以 Java 进程的身份运行,所以这个功能必须先有一个能"关住"代码的执行环境。环境有两种接法,由 kb.python.runner 选择,默认 docker:

runner适用场景隔离方式是否需要 Docker
dockerWindows 与 Linux;引擎可以在本机,也可以在局域网内另一台主机一次性容器:断网、只读根、非 root、内存/CPU/进程数上限、临时目录走内存是(本机或远程任一)
hostLinux 主机(包括容器内运行的 Linux 部署)runuser 切到沙箱用户,在独立目录里执行,环境变量白名单否

本篇只讲环境怎么准备、怎么验证、边界在哪里;执行器本身的协议、参数和失败语义见隔离 Python 执行器,函数库接口见函数库与调用接口。

配置项 ​

全部写在 mosskb-web/my.txt:

配置默认值作用
kb.python.runnerdockerdocker 或 host
kb.python.docker取 PATH 上的 dockerdocker 客户端可执行文件路径
kb.python.docker.host空Docker 引擎地址,例如 tcp://192.168.31.97:2375;空表示本机引擎
kb.python.wsl.distribution空用 WSL 里的 Docker 引擎时填发行版名,与 kb.python.docker 二选一
kb.python.imagepython:3.12-slim执行容器镜像;执行时固定 --pull=never,镜像必须提前准备好
kb.python.timeout_seconds15单次执行超时秒数,范围 1~60
kb.python.sandbox.usersandboxhost runner 的沙箱用户
kb.python.sandbox.dir/var/lib/java-mosskb/sandboxhost runner 的沙箱目录,同时作为子进程工作目录
kb.python.sandbox.pythonpython3host 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 单独可用,版本与目标引擎保持一致即可。把路径写进配置:

properties
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 执行一次:

bash
./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转发

shell
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
shell
iptables -I DOCKER-USER -p tcp --dport 2375 ! -s 192.168.31.225 -j DROP

192.168.31.225换成你的本机的IP

shell
curl -s http://127.0.0.1:2375/_ping

准备镜像 ​

执行固定使用 --pull=never,镜像必须提前拉好,否则会得到"容器启动或执行失败":

powershell
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 标准环境,不会按函数代码的请求去安装依赖。

验证 ​

powershell
.\scripts\Test-PythonSandbox.ps1

脚本默认读取 my.txt 里的实际配置(也可用参数临时覆盖),在容器内检查四件事:进程 uid 是否为 65534、根文件系统是否只读、外部网络是否不可达、GITEE_API_KEY 之类的宿主密钥是否不可见。四项都通过才说明这条链路可用。

二、Linux:使用本机 Docker ​

服务器上已经装好 Docker 时,这是最省事的接法:后端直接跑在宿主上,执行器调用的就是本机的 docker。my.txt 里只需要两行:

properties
kb.python.runner=docker
kb.python.image=python:3.12-slim

kb.python.docker 留空就取 PATH 上的 docker;要显式指定就写 kb.python.docker=/usr/bin/docker。kb.python.wsl.distribution 也不要设,它是给 Windows 本机的 WSL 引擎用的。

从开发机搬过来的 my.txt 必须删掉这两行,它们只在开发机成立:

properties
kb.python.docker=D:/tools/docker/bin/docker.exe
kb.python.docker.host=tcp://192.168.31.97:2375

第一行是 Windows 路径,Linux 上的进程无法启动这个可执行文件,ProcessBuilder.start() 抛出 IOException,接口直接返回"隔离 Python 执行器不可用,请先安装并启动 Docker;不会在宿主机执行代码"。这条文案只表示 docker 客户端没能启动,与 daemon 是否运行、镜像是否拉取无关:daemon 未启动、当前用户没有 socket 权限、镜像不存在这三类问题报的是另一条"Python 容器启动或执行失败,请检查 Docker 服务及隔离镜像"。第二行会把 --host 指向开发内网的引擎,服务器连不上时同样落到后一条文案上。

一次性准备 ​

bash
docker pull python:3.12-slim           # 执行固定用 --pull=never,镜像必须先拉好
sudo usermod -aG docker <后端运行用户>   # 不加组会因 /var/run/docker.sock 权限失败

改完 my.txt 后重启 Java 服务:配置在启动时读取一次,不会热加载。KB_PYTHON_RUNNER、KB_PYTHON_DOCKER、KB_PYTHON_DOCKER_HOST 这类环境变量(键里的点换成下划线再大写)优先级高于 my.txt,容器化部署可以直接用 -e 注入而不改文件。

验证 ​

先以运行后端的同一个用户确认客户端可用,Client 与 Server 都要有输出:

bash
sudo -u <后端运行用户> docker version

再手工按执行器实际使用的限制参数启动一次容器:

bash
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。最后在函数库中调试一个返回 a + b 的函数,确认 Java 到容器的整条链路,接口见函数库与调用接口。服务器上还没装 Docker 时,scripts/setup-python-sandbox.sh 把安装 Docker Engine、拉镜像和这次容器验证合成了一步。

后端本身跑在容器里 ​

这时宿主装了 Docker 也没用:执行器是在后端进程所在的环境里调用 docker 客户端,而镜像里通常没有这个可执行文件,报错与上面完全一样。三种接法:

接法做法代价
挂载宿主 socket镜像里装 docker CLI,容器加 -v /var/run/docker.sock:/var/run/docker.sock,并保证容器内用户能访问该 socket容器等于拿到宿主 root 权限
远程引擎-e KB_PYTHON_DOCKER_HOST=tcp://<装了 Docker 的机器>:2375,在那台机器上拉好镜像并按第一节开放接口明文 2375 无认证,仅适合可信内网
容器内宿主沙箱镜像补 python3 与 util-linux,容器以 --user root 运行,再按下一节准备沙箱并设 kb.python.runner=host失去断网、只读根与资源上限

三、Linux:不用 Docker 的宿主沙箱 ​

Linux 上可以直接让用户代码以另一个低权限用户运行,不需要容器引擎。以 root 执行一次:

bash
./scripts/setup-python-host-sandbox.sh sandbox /var/lib/java-mosskb/sandbox python3

脚本会创建一个系统用户(不允许登录、home 指向沙箱目录,默认是 sandbox);创建沙箱目录并设成 0750、属主为该用户;然后用 runuser 实际切过去执行一次,确认拿到的 uid 就是沙箱用户,最后打印要写进 my.txt 的配置。

properties
kb.python.runner=host
kb.python.sandbox.user=sandbox
kb.python.sandbox.dir=/var/lib/java-mosskb/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 的命令形状是:

bash
/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-mosskb 进程自身以 root 运行(runuser 需要权限才能切换用户);
  • 沙箱目录存在,且不是所有人可写;
  • 能找到 runuser,或者已显式配置 kb.python.sandbox.su。

四、两种环境的边界对比 ​

这是选型时最该看的一张表:

保证docker runnerhost runner
不以 Java 进程身份运行是是
宿主密钥不可见是(环境变量白名单)是(环境变量白名单)
断网是(--network=none)否
根文件系统只读是否
内存/CPU/进程数上限是(256 MiB / 1 CPU / 32)否
一次性文件系统是(容器 + tmpfs)否,沙箱目录长期存在
执行超时后终止是是(连同用户代码子进程整棵树终止)
能读宿主机其它文件否(不挂载宿主目录)能读 others 可读的文件

host runner 的代价很具体:沙箱用户可以读到宿主上任何"其他人可读"的文件。因此 mosskb-web/my.txt、mosskb-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 端口。

text
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 终端中执行:

powershell
DISM /Online /Enable-Feature /FeatureName:Microsoft-Windows-Subsystem-Linux /All /NoRestart
DISM /Online /Enable-Feature /FeatureName:VirtualMachinePlatform /All /NoRestart

两条命令成功后,先通过「开始 → 电源 → 重启」重启 Windows。/NoRestart 只是禁止命令自动重启,不能省略这一步。然后执行:

powershell
wsl --update
wsl --set-default-version 2
wsl --install -d Ubuntu

如果 Ubuntu 应用已经安装,直接打开它完成初始化即可。首次启动会提示创建 UNIX 用户,例如 ping;该用户名不需要与 Windows 用户名一致。输入密码时终端不显示字符,重复输入后看到 passwd: password updated successfully 表示密码设置成功;随后出现的 usermod: no changes 本身不表示安装失败。

WSL 安装后的欢迎页面

上图是 WSL 欢迎页面。发行版是否安装成功、是否使用 WSL2,应以 Windows 终端中的检查结果为准:

powershell
wsl --status
wsl --list --verbose

列表中应能看到 Ubuntu,且 VERSION 为 2。如果为 1,执行 wsl --set-version Ubuntu 2;如果提示没有已安装的发行版,说明当前 Windows 用户下还没有成功注册 Ubuntu,需要完成安装及首次初始化。后续命令中的 Ubuntu 必须与列表里的发行版名称一致。

2. 安装 Docker Engine 和 Python 镜像 ​

脚本安装和手动安装二选一,完成后都继续检查引擎、配置 Java 并验证隔离效果。

方式一:使用项目脚本 ​

在 Java 项目的 java-mosskb 根目录打开 PowerShell,执行项目自带的安装脚本:

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:

powershell
wsl -d Ubuntu

以下 bash 命令全部在 Ubuntu 终端执行,不要粘贴到 Windows CMD 或 PowerShell。 普通用户通过 sudo 提权,输入的是初始化 Ubuntu 时设置的 Linux 密码。

(1)准备软件源。 新安装的 Ubuntu 可直接继续。如果此前安装过 docker.io、podman-docker 或单独的 containerd、runc,先按 Docker 官方安装文档处理冲突软件包,再安装官方 Docker Engine。

bash
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 必须独占一行:

bash
. /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 并启动服务。

bash
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 镜像并测试容器。

bash
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 终端检查引擎和镜像:

powershell
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 项目的 mosskb-web/my.txt:

properties
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-mosskb 根目录的 PowerShell 中执行:

powershell
.\scripts\Test-PythonSandbox.ps1

脚本读取 mosskb-web/my.txt,输出的 engine 应包含 wsl.exe --distribution Ubuntu --user root --exec docker,且不带旧的远程 --host。验证成功时,结果应满足:

json
{
  "uid": 65534,
  "secret_absent": true,
  "read_only": true,
  "network_blocked": true
}

最后在函数库中调试一个简单函数,例如返回 a + b,确认 Java 到容器的完整调用链路正常;接口操作见函数库与调用接口。

5. 重启 WSL 后启动 Docker ​

如果 docker version 只有 Client 信息,并提示无法连接 Docker daemon,先在 Windows 终端执行:

powershell
wsl -d Ubuntu -u root --exec service docker start

需要让 Docker 随 Ubuntu 启动时,可使用 systemd。在 Ubuntu 中执行 ps -p 1 -o comm=;如果结果不是 systemd,编辑 /etc/wsl.conf,在保留已有配置的基础上添加或合并:

ini
[boot]
systemd=true

保存 Linux 中的工作后,在 Windows 终端执行 wsl --shutdown,再打开 Ubuntu。该命令会停止所有正在运行的 WSL 发行版。随后在 Ubuntu 中执行:

bash
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager

这表示 Docker 随该 WSL 发行版启动,并不等于 Windows 开机后 WSL 一定已经运行。systemd 的版本要求和配置说明见微软 WSL systemd 文档。

6. 安装时的常见故障 ​

启用 Windows 功能时报 0x800f081f: 表示缺少组件源文件。在管理员 Windows 终端执行:

powershell
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 中执行以下命令,然后重启:

cmd
bcdedit /set hypervisorlaunchtype auto

该命令设置 Windows 虚拟机监控程序随系统启动,不能代替 BIOS/UEFI 的虚拟化开关。如果 Windows 本身运行在虚拟机内,还需要宿主平台提供嵌套虚拟化。详见微软 WSL 故障排查。

相关章节 ​

MIT Licensed