Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agents/hermes/policy-additions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ filesystem_policy:
- /app
- /etc
- /var/log
- /var/lib/dpkg # Allow package-version inspection without package mutation.
read_write:
- /sandbox
- /tmp
Expand Down
1 change: 1 addition & 0 deletions agents/hermes/policy-permissive.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ filesystem_policy:
- /app
- /etc
- /var/log
- /var/lib/dpkg # Allow package-version inspection without package mutation.
read_write:
- /sandbox
- /tmp
Expand Down
1 change: 1 addition & 0 deletions agents/langchain-deepagents-code/policy-additions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ filesystem_policy:
- /app
- /etc
- /var/log
- /var/lib/dpkg # Allow package-version inspection without package mutation.
read_write:
- /sandbox
- /sandbox/.deepagents
Expand Down
1 change: 1 addition & 0 deletions agents/openclaw/policy-permissive.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ filesystem_policy:
- /app
- /etc
- /var/log
- /var/lib/dpkg # Allow package-version inspection without package mutation.
read_write:
- /tmp
- /dev/null
Expand Down
5 changes: 4 additions & 1 deletion docs/reference/network-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ Deep Agents uses strict Landlock compatibility, so sandbox startup fails when Op
| Path | Access |
|---|---|
| `/sandbox`, `/tmp`, `/dev/null`, `/dev/pts` | Read-write |
| `/usr`, `/lib`, `/proc`, `/dev/urandom`, `/app`, `/etc`, `/var/log` | Read-only |
| `/usr`, `/lib`, `/proc`, `/dev/urandom`, `/app`, `/etc`, `/var/log`, `/var/lib/dpkg` | Read-only |

`/dev/pts` is the pseudo-terminal (devpts) directory.
It is writable so PTY-based tools (`tmux`, `script`, and interactive shells) can allocate a terminal.
Without it, those tools fail with `fork failed: Permission denied`.

Read-only access to `/var/lib/dpkg` lets `dpkg-query` inspect installed package metadata.
The filesystem policy does not grant write access to the package database.

The sandbox process runs as a dedicated `sandbox` user and group.
<AgentOnly variant="openclaw">
Landlock LSM enforcement applies on a best-effort basis.
Expand Down
2 changes: 1 addition & 1 deletion docs/security/best-practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ The container mounts system directories read-only to prevent the agent from modi

| Aspect | Detail |
|---|---|
| Default | `/usr`, `/lib`, `/proc`, `/dev/urandom`, `/app`, `/etc`, `/var/log` are read-only. |
| Default | `/usr`, `/lib`, `/proc`, `/dev/urandom`, `/app`, `/etc`, `/var/log`, and `/var/lib/dpkg` are read-only. |
| What you can change | Add or remove paths in the `filesystem_policy.read_only` section of the policy file. |
| Risk if relaxed | Making `/usr` or `/lib` writable lets the agent replace system binaries (such as `curl` or `node`) with trojanized versions. Making `/etc` writable lets the agent modify DNS resolution, TLS trust stores, or user accounts. |
| Recommendation | Never make system paths writable. If the agent needs a writable location for generated files, use a subdirectory of `/sandbox`. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ filesystem_policy:
- /app
- /etc
- /var/log
- /var/lib/dpkg # Allow package-version inspection without package mutation.
read_write:
- /tmp
- /dev/null
Expand Down
1 change: 1 addition & 0 deletions nemoclaw-blueprint/policies/openclaw-sandbox.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ filesystem_policy:
- /app
- /etc
- /var/log
- /var/lib/dpkg # Allow package-version inspection without package mutation.
read_write:
- /tmp
- /dev/null
Expand Down
33 changes: 32 additions & 1 deletion src/lib/onboard/initial-policy-real-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type PolicyEntry = {
};

type PolicyDocument = {
filesystem_policy?: { read_write?: string[] };
filesystem_policy?: { read_only?: string[]; read_write?: string[] };
network_policies?: Record<string, PolicyEntry>;
};

Expand Down Expand Up @@ -140,6 +140,37 @@ describe("initial sandbox policy real preset merge", () => {
}
});

it("grants read-only package database access in every shipping sandbox policy (#8467)", () => {
const policyCases = [
{ path: ["nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"], agent: "openclaw" },
{
path: ["nemoclaw-blueprint", "policies", "openclaw-sandbox-permissive.yaml"],
agent: "openclaw",
},
{ path: ["agents", "openclaw", "policy-permissive.yaml"], agent: "openclaw" },
{ path: ["agents", "hermes", "policy-additions.yaml"], agent: "hermes" },
{ path: ["agents", "hermes", "policy-permissive.yaml"], agent: "hermes" },
{
path: ["agents", "langchain-deepagents-code", "policy-additions.yaml"],
agent: "langchain-deepagents-code",
},
];

for (const policyCase of policyCases) {
const prepared = prepareInitialSandboxCreatePolicy(repoPath(...policyCase.path), [], {
agentName: policyCase.agent,
});
const policy = readPreparedPolicy(prepared);
const readOnly = policy.filesystem_policy?.read_only ?? [];
const readWrite = policy.filesystem_policy?.read_write ?? [];

expect(readOnly, policyCase.path.join("/")).toContain("/var/lib/dpkg");
for (const writableAncestor of ["/", "/var", "/var/lib", "/var/lib/dpkg"]) {
expect(readWrite, policyCase.path.join("/")).not.toContain(writableAncestor);
}
}
});

Comment thread
coderabbitai[bot] marked this conversation as resolved.
it("preserves baseline writable paths in effective OpenClaw permissive create policies", () => {
const baseline = readPreparedPolicy(
prepareInitialSandboxCreatePolicy(
Expand Down
29 changes: 27 additions & 2 deletions test/e2e/live/network-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@ async function sandboxBash(
});
}

async function expectPackageDatabaseReadOnly(sandbox: SandboxClient): Promise<void> {
const probe = await sandboxBash(
sandbox,
String.raw`
set -euo pipefail
dpkg-query -W dpkg
printf 'DPKG_QUERY_OK\n'
if touch /var/lib/dpkg/nemoclaw-e2e-write-probe 2>/tmp/nemoclaw-dpkg-write-error; then
printf 'DPKG_WRITE_UNEXPECTEDLY_SUCCEEDED\n'
exit 1
fi
printf 'DPKG_WRITE_DENIED\n'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
`,
{ artifactName: "tc-net-dpkg-package-database-read-only" },
);
expect(probe.exitCode, text(probe)).toBe(0);
expect(text(probe)).toContain("DPKG_QUERY_OK");
expect(text(probe)).toContain("DPKG_WRITE_DENIED");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

async function applyPreset(host: HostCliClient, preset: string): Promise<ShellProbeResult> {
const result = await runNemoclaw(host, [SANDBOX_NAME, "policy-add", preset, "--yes"], {
artifactName: `policy-add-${preset}`,
Expand Down Expand Up @@ -509,7 +529,7 @@ test("network-policy: restricted sandbox enforces live allow/deny policy probes"
e2ePhases: [
"confirm built CLI Docker OpenShell and credential",
"clear the sandbox and onboard restricted policy",
"prove zero active presets, default denial, and the weather allowlist",
"prove zero active presets, read-only package metadata, default denial, and the weather allowlist",
"exercise package and SaaS policy presets",
"prove dry-run and per-binary Jira approval",
"verify hot reload inference exemption and SSRF guards",
Expand All @@ -524,6 +544,7 @@ test("network-policy: restricted sandbox enforces live allow/deny policy probes"
contracts: [
"deny-by-default egress",
"restricted tier begins with zero active presets",
"package metadata is readable while package database writes remain denied (#8467)",
"OpenShell 0.0.85 preserves the full denied endpoint and policy disposition through nemoclaw logs --tail 50 (#4760)",
"read-only preset allowlist behavior",
"weather preset allows wttr.in GET and HEAD but denies POST and unrelated hosts",
Expand Down Expand Up @@ -645,7 +666,9 @@ test("network-policy: restricted sandbox enforces live allow/deny policy probes"

// Keep the actual OpenShell boundary in the retained journey: a default
// restricted onboard must have no active preset before operator mutation.
progress.phase("prove zero active presets, default denial, and the weather allowlist");
progress.phase(
"prove zero active presets, read-only package metadata, default denial, and the weather allowlist",
);
const policyListAfterOnboard = await runNemoclaw(host, [SANDBOX_NAME, "policy-list"], {
artifactName: "tc-net-01-policy-list-after-onboard",
timeoutMs: SANDBOX_EXEC_TIMEOUT_MS,
Expand All @@ -664,6 +687,8 @@ test("network-policy: restricted sandbox enforces live allow/deny policy probes"
).not.toBeNull();
expect(activePresets?.length, "restricted tier must begin with zero active presets").toBe(0);

await expectPackageDatabaseReadOnly(sandbox);

const denyDefault = await fetchStatus(sandbox, "https://example.com/", "tc-net-01-deny-default");
expect(denyDefault, `example.com should be blocked under restricted policy`).toMatch(
/STATUS_403|ERROR_/,
Expand Down
Loading