mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(sandbox): reject unsafe rootless Podman users
This commit is contained in:
@@ -149,7 +149,7 @@ Use `sandbox.backend: "podman"` to select the native `podman` CLI directly. This
|
||||
|
||||
Podman reuses the existing `sandbox.docker.*` settings and the active native `podman` CLI context; it adds no separate connection configuration surface.
|
||||
|
||||
Rootless Podman defaults to `--userns=keep-id` for writable workspace mounts. A long-lived sandbox can reserve subordinate IDs and block unrelated `--userns=auto` workloads; remove it before starting those workloads. Set `sandbox.docker.user` to control the container user; rootful Podman otherwise uses the workspace owner when available.
|
||||
Rootless Podman defaults to `--userns=keep-id` for writable workspace mounts. A long-lived sandbox can reserve subordinate IDs and block unrelated `--userns=auto` workloads; remove it before starting those workloads. Set `sandbox.docker.user` to a nonzero numeric UID or UID:GID to control the container user. Rootless Podman rejects UID or GID 0 because Podman 4.x cannot remap namespace root while preserving workspace bind ownership; bake root-required setup into the image or use rootful Podman. Rootful Podman otherwise uses the workspace owner when available.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -523,9 +523,9 @@ Paths:
|
||||
- Default `docker.network` is `"none"` (no egress), so package installs will fail.
|
||||
- `docker.network: "container:<id>"` requires `dangerouslyAllowContainerNamespaceJoin: true` and is break-glass only.
|
||||
- `readOnlyRoot: true` prevents writes; set `readOnlyRoot: false` or bake a custom image.
|
||||
- `user` must be root for package installs. Docker can omit `user` or set
|
||||
`user: "0:0"`; rootless Podman must set `user: "0:0"` because omission
|
||||
defaults to the invoking user through `--userns=keep-id`.
|
||||
- `user` must be root for package installs. Docker or rootful Podman can omit
|
||||
`user` or set `user: "0:0"`. Rootless Podman rejects zero-valued users;
|
||||
bake packages into the image or use rootful Podman.
|
||||
- Sandbox exec does **not** inherit host `process.env`. Use `agents.defaults.sandbox.docker.env` (or a custom image) for skill API keys.
|
||||
- Values in `agents.defaults.sandbox.docker.env` are passed as explicit container environment variables. Anyone with access to the selected container engine can inspect them with metadata commands such as `docker inspect` or `podman inspect`. Use a custom image, mounted secret file, or another secret delivery path if that metadata exposure is not acceptable.
|
||||
|
||||
|
||||
@@ -681,20 +681,32 @@ describe("ensureSandboxContainer config-hash recreation", () => {
|
||||
expect(collectDockerFlagValues(createCall.args, "--userns")).toEqual([]);
|
||||
});
|
||||
|
||||
it("maps an explicit root user through rootless Podman keep-id", async () => {
|
||||
const cfg = createSandboxConfig([]);
|
||||
cfg.docker.user = "0:0";
|
||||
spawnState.inspectRunning = false;
|
||||
registryMocks.readRegistryEntry.mockResolvedValue(null);
|
||||
it.each([{ user: "0" }, { user: "0:0" }, { user: "1001:0" }, { user: "0:1002" }])(
|
||||
"rejects zero-valued rootless Podman user $user",
|
||||
async ({ user }) => {
|
||||
const cfg = createSandboxConfig([]);
|
||||
cfg.docker.user = user;
|
||||
spawnState.inspectRunning = false;
|
||||
registryMocks.readRegistryEntry.mockResolvedValue(null);
|
||||
|
||||
const createCall = await ensureSandboxCreateCallForTest({
|
||||
cfg,
|
||||
engine: PODMAN_SANDBOX_ENGINE,
|
||||
});
|
||||
await expect(
|
||||
ensureSandboxContainer({
|
||||
engine: PODMAN_SANDBOX_ENGINE,
|
||||
scopeKey: "shared",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentWorkspaceDir: "/tmp/workspace",
|
||||
cfg,
|
||||
}),
|
||||
).rejects.toThrow(/cannot use UID or GID 0/iu);
|
||||
|
||||
expect(collectDockerFlagValues(createCall.args, "--user")).toEqual(["0:0"]);
|
||||
expect(collectDockerFlagValues(createCall.args, "--userns")).toEqual(["keep-id:uid=0,gid=0"]);
|
||||
});
|
||||
expect(spawnState.calls).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
command: "podman",
|
||||
args: expect.arrayContaining(["create"]),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects nonnumeric users for rootless Podman keep-id", async () => {
|
||||
const cfg = createSandboxConfig([]);
|
||||
@@ -943,7 +955,7 @@ describe("ensureSandboxContainer config-hash recreation", () => {
|
||||
createArgsEpoch: SANDBOX_DOCKER_CREATE_ARGS_EPOCH,
|
||||
readOnlyWorkspaceSkillMounts: [],
|
||||
});
|
||||
const oldHash = `${genericHash}:podman-runtime-v6:keep-id:default`;
|
||||
const oldHash = `${genericHash}:podman-runtime-v8:keep-id:default`;
|
||||
cfg.dockerTmpfsSource = "configured";
|
||||
spawnState.inspectRunning = false;
|
||||
spawnState.labelHash = oldHash;
|
||||
|
||||
@@ -42,6 +42,11 @@ function resolvePodmanKeepIdMode(user: string | undefined): string {
|
||||
);
|
||||
}
|
||||
const [, uid, gid] = match;
|
||||
if (uid === "0" || gid === "0") {
|
||||
throw invalidPodmanConfig(
|
||||
`Rootless Podman sandbox user "${normalized}" cannot use UID or GID 0 while preserving workspace bind ownership. Bake root-required setup into the image or use rootful Podman.`,
|
||||
);
|
||||
}
|
||||
return gid ? `keep-id:uid=${uid},gid=${gid}` : `keep-id:uid=${uid}`;
|
||||
}
|
||||
|
||||
@@ -287,7 +292,7 @@ export function resolvePodmanSandboxConfigHash(params: {
|
||||
dockerTmpfsSource: SandboxConfig["dockerTmpfsSource"];
|
||||
}): string {
|
||||
const userMode = params.configuredUser ? "configured-user" : "keep-id";
|
||||
return `${params.genericConfigHash}:podman-runtime-v6:${userMode}:${params.dockerTmpfsSource}`;
|
||||
return `${params.genericConfigHash}:podman-runtime-v8:${userMode}:${params.dockerTmpfsSource}`;
|
||||
}
|
||||
|
||||
export function resolvePodmanSandboxContainerPrefix(containerPrefix: string): string {
|
||||
|
||||
Reference in New Issue
Block a user