mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
refactor(sandbox): centralize network mode policy helpers
This commit is contained in:
28
src/agents/sandbox/network-mode.ts
Normal file
28
src/agents/sandbox/network-mode.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export type NetworkModeBlockReason = "host" | "container_namespace_join";
|
||||
|
||||
export function normalizeNetworkMode(network: string | undefined): string | undefined {
|
||||
const normalized = network?.trim().toLowerCase();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
export function getBlockedNetworkModeReason(params: {
|
||||
network: string | undefined;
|
||||
allowContainerNamespaceJoin?: boolean;
|
||||
}): NetworkModeBlockReason | null {
|
||||
const normalized = normalizeNetworkMode(params.network);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (normalized === "host") {
|
||||
return "host";
|
||||
}
|
||||
if (normalized.startsWith("container:") && params.allowContainerNamespaceJoin !== true) {
|
||||
return "container_namespace_join";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isDangerousNetworkMode(network: string | undefined): boolean {
|
||||
const normalized = normalizeNetworkMode(network);
|
||||
return normalized === "host" || normalized?.startsWith("container:") === true;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
normalizeSandboxHostPath,
|
||||
resolveSandboxHostPathViaExistingAncestor,
|
||||
} from "./host-paths.js";
|
||||
import { getBlockedNetworkModeReason } from "./network-mode.js";
|
||||
|
||||
// Targeted denylist: host paths that should never be exposed inside sandbox containers.
|
||||
// Exported for reuse in security audit collectors.
|
||||
@@ -31,7 +32,6 @@ export const BLOCKED_HOST_PATHS = [
|
||||
"/run/docker.sock",
|
||||
];
|
||||
|
||||
const BLOCKED_NETWORK_MODES = new Set(["host"]);
|
||||
const BLOCKED_SECCOMP_PROFILES = new Set(["unconfined"]);
|
||||
const BLOCKED_APPARMOR_PROFILES = new Set(["unconfined"]);
|
||||
const RESERVED_CONTAINER_TARGET_PATHS = ["/workspace", SANDBOX_AGENT_WORKSPACE_MOUNT];
|
||||
@@ -284,12 +284,11 @@ export function validateNetworkMode(
|
||||
network: string | undefined,
|
||||
options?: ValidateNetworkModeOptions,
|
||||
): void {
|
||||
const normalized = network?.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (BLOCKED_NETWORK_MODES.has(normalized)) {
|
||||
const blockedReason = getBlockedNetworkModeReason({
|
||||
network,
|
||||
allowContainerNamespaceJoin: options?.allowContainerNamespaceJoin,
|
||||
});
|
||||
if (blockedReason === "host") {
|
||||
throw new Error(
|
||||
`Sandbox security: network mode "${network}" is blocked. ` +
|
||||
'Network "host" mode bypasses container network isolation. ' +
|
||||
@@ -297,7 +296,7 @@ export function validateNetworkMode(
|
||||
);
|
||||
}
|
||||
|
||||
if (normalized.startsWith("container:") && options?.allowContainerNamespaceJoin !== true) {
|
||||
if (blockedReason === "container_namespace_join") {
|
||||
throw new Error(
|
||||
`Sandbox security: network mode "${network}" is blocked by default. ` +
|
||||
'Network "container:*" joins another container namespace and bypasses sandbox network isolation. ' +
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSandboxBrowserConfig } from "../agents/sandbox/config.js";
|
||||
import {
|
||||
resolveSandboxBrowserConfig,
|
||||
resolveSandboxDockerConfig,
|
||||
} from "../agents/sandbox/config.js";
|
||||
import { validateConfigObject } from "./config.js";
|
||||
|
||||
describe("sandbox docker config", () => {
|
||||
@@ -84,6 +87,22 @@ describe("sandbox docker config", () => {
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("uses agent override precedence for dangerouslyAllowContainerNamespaceJoin", () => {
|
||||
const inherited = resolveSandboxDockerConfig({
|
||||
scope: "agent",
|
||||
globalDocker: { dangerouslyAllowContainerNamespaceJoin: true },
|
||||
agentDocker: {},
|
||||
});
|
||||
expect(inherited.dangerouslyAllowContainerNamespaceJoin).toBe(true);
|
||||
|
||||
const overridden = resolveSandboxDockerConfig({
|
||||
scope: "agent",
|
||||
globalDocker: { dangerouslyAllowContainerNamespaceJoin: true },
|
||||
agentDocker: { dangerouslyAllowContainerNamespaceJoin: false },
|
||||
});
|
||||
expect(overridden.dangerouslyAllowContainerNamespaceJoin).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects seccomp unconfined via Zod schema validation", () => {
|
||||
const res = validateConfigObject({
|
||||
agents: {
|
||||
|
||||
@@ -299,6 +299,10 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"agents.defaults.sandbox.browser.network":
|
||||
"Docker network for sandbox browser containers (default: openclaw-sandbox-browser). Avoid bridge if you need stricter isolation.",
|
||||
"agents.list[].sandbox.browser.network": "Per-agent override for sandbox browser Docker network.",
|
||||
"agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin":
|
||||
"DANGEROUS break-glass override that allows sandbox Docker network mode container:<id>. This joins another container namespace and weakens sandbox isolation.",
|
||||
"agents.list[].sandbox.docker.dangerouslyAllowContainerNamespaceJoin":
|
||||
"Per-agent DANGEROUS override for container namespace joins in sandbox Docker network mode.",
|
||||
"agents.defaults.sandbox.browser.cdpSourceRange":
|
||||
"Optional CIDR allowlist for container-edge CDP ingress (for example 172.21.0.1/32).",
|
||||
"agents.list[].sandbox.browser.cdpSourceRange":
|
||||
|
||||
@@ -405,6 +405,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.heartbeat.suppressToolErrorWarnings": "Heartbeat Suppress Tool Error Warnings",
|
||||
"agents.defaults.sandbox.browser.network": "Sandbox Browser Network",
|
||||
"agents.defaults.sandbox.browser.cdpSourceRange": "Sandbox Browser CDP Source Port Range",
|
||||
"agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin":
|
||||
"Sandbox Docker Allow Container Namespace Join",
|
||||
commands: "Commands",
|
||||
"commands.native": "Native Commands",
|
||||
"commands.nativeSkills": "Native Skill Commands",
|
||||
@@ -713,6 +715,8 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"Agent Heartbeat Suppress Tool Error Warnings",
|
||||
"agents.list[].sandbox.browser.network": "Agent Sandbox Browser Network",
|
||||
"agents.list[].sandbox.browser.cdpSourceRange": "Agent Sandbox Browser CDP Source Port Range",
|
||||
"agents.list[].sandbox.docker.dangerouslyAllowContainerNamespaceJoin":
|
||||
"Agent Sandbox Docker Allow Container Namespace Join",
|
||||
"discovery.mdns.mode": "mDNS Discovery Mode",
|
||||
plugins: "Plugins",
|
||||
"plugins.enabled": "Enable Plugins",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { getBlockedNetworkModeReason } from "../agents/sandbox/network-mode.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
import { AgentModelSchema } from "./zod-schema.agent-model.js";
|
||||
import {
|
||||
@@ -154,8 +155,11 @@ export const SandboxDockerSchema = z
|
||||
}
|
||||
}
|
||||
}
|
||||
const network = data.network?.trim().toLowerCase();
|
||||
if (network === "host") {
|
||||
const blockedNetworkReason = getBlockedNetworkModeReason({
|
||||
network: data.network,
|
||||
allowContainerNamespaceJoin: data.dangerouslyAllowContainerNamespaceJoin === true,
|
||||
});
|
||||
if (blockedNetworkReason === "host") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["network"],
|
||||
@@ -163,7 +167,7 @@ export const SandboxDockerSchema = z
|
||||
'Sandbox security: network mode "host" is blocked. Use "bridge" or "none" instead.',
|
||||
});
|
||||
}
|
||||
if (network?.startsWith("container:") && data.dangerouslyAllowContainerNamespaceJoin !== true) {
|
||||
if (blockedNetworkReason === "container_namespace_join") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["network"],
|
||||
@@ -476,11 +480,11 @@ export const AgentSandboxSchema = z
|
||||
})
|
||||
.strict()
|
||||
.superRefine((data, ctx) => {
|
||||
const browserNetwork = data.browser?.network?.trim().toLowerCase();
|
||||
if (
|
||||
browserNetwork?.startsWith("container:") &&
|
||||
data.docker?.dangerouslyAllowContainerNamespaceJoin !== true
|
||||
) {
|
||||
const blockedBrowserNetworkReason = getBlockedNetworkModeReason({
|
||||
network: data.browser?.network,
|
||||
allowContainerNamespaceJoin: data.docker?.dangerouslyAllowContainerNamespaceJoin === true,
|
||||
});
|
||||
if (blockedBrowserNetworkReason === "container_namespace_join") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["browser", "network"],
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
resolveSandboxConfigForAgent,
|
||||
resolveSandboxToolPolicyForAgent,
|
||||
} from "../agents/sandbox.js";
|
||||
import { isDangerousNetworkMode, normalizeNetworkMode } from "../agents/sandbox/network-mode.js";
|
||||
/**
|
||||
* Synchronous security audit collector functions.
|
||||
*
|
||||
@@ -830,8 +831,8 @@ export function collectSandboxDangerousConfigFindings(cfg: OpenClawConfig): Secu
|
||||
}
|
||||
|
||||
const network = typeof docker.network === "string" ? docker.network : undefined;
|
||||
const normalizedNetwork = network?.trim().toLowerCase();
|
||||
if (normalizedNetwork === "host" || normalizedNetwork?.startsWith("container:")) {
|
||||
const normalizedNetwork = normalizeNetworkMode(network);
|
||||
if (isDangerousNetworkMode(network)) {
|
||||
const modeLabel = normalizedNetwork === "host" ? '"host"' : `"${network}"`;
|
||||
const detail =
|
||||
normalizedNetwork === "host"
|
||||
|
||||
Reference in New Issue
Block a user