Skip to content

Commit 60a282d

Browse files
authored
fix(policy): switch npm preset to L4 tunnel for undici CONNECT compatibility (#3024)
Switch npm/Yarn registry policy to L4 pass-through so Node 22 undici can use CONNECT tunnels through HTTPS_PROXY without REST-mode method inspection resetting tarball downloads.\n\nKeep PyPI on REST GET/HEAD rules, add regression coverage for npm L4 behavior, and update the security guidance to document the npm-specific L4 compatibility exception.\n\nSigned-off-by: Yimo Jiang <yimoj@nvidia.com>\nSigned-off-by: Aaron Erickson <aerickson@nvidia.com>
1 parent eed5c46 commit 60a282d

4 files changed

Lines changed: 78 additions & 28 deletions

File tree

docs/security/best-practices.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ Endpoint rules restrict allowed HTTP methods and URL paths.
161161

162162
| Aspect | Detail |
163163
|---|---|
164-
| Default | Some endpoints allow GET and POST on `/**` (for example, `clawhub.ai`). Others restrict methods and paths to specific API routes (for example, `integrate.api.nvidia.com` allows POST only to inference and embedding paths and GET to model listings). Read-only endpoints such as `docs.openclaw.ai` allow GET only. The `npm_registry` baseline entry and the `npm`/`pypi` presets are GET-only (plus HEAD for PyPI). |
164+
| Default | Some endpoints allow GET and POST on `/**` (for example, `clawhub.ai`). Others restrict methods and paths to specific API routes (for example, `integrate.api.nvidia.com` allows POST only to inference and embedding paths and GET to model listings). Read-only endpoints such as `docs.openclaw.ai`, the `npm_registry` baseline entry, and the `pypi` preset allow GET only (PyPI also allows HEAD). The `npm` preset is an intentional exception: npm/Yarn registry traffic uses L4 pass-through for Node 22 undici CONNECT compatibility. |
165165
| What you can change | Add methods (PUT, DELETE, PATCH) or restrict paths to specific prefixes. |
166166
| Risk if relaxed | Allowing all methods on an API endpoint gives the agent write and delete access. For example, allowing DELETE on `api.github.com` lets the agent delete repositories. |
167167
| Recommendation | Use GET-only rules for endpoints that the agent only reads. Add write methods only for endpoints where the agent must create or modify resources. Restrict paths to specific API routes when possible. |
@@ -176,7 +176,7 @@ The `protocol` field on an endpoint controls whether the proxy also inspects ind
176176
| Default | Endpoints without a `protocol` field use L4-only enforcement: the proxy checks host, port, and binary identity, then relays the TCP stream without inspecting payloads. Setting `protocol: rest` enables L7 inspection: the proxy auto-detects and terminates TLS, then evaluates each HTTP request's method and path against the endpoint's `rules` or `access` preset. |
177177
| What you can change | Add `protocol: rest` to an endpoint to enable per-request HTTP inspection. Use the `access` preset (`full`, `read-only`, `read-write`) or explicit `rules` to control allowed methods and paths. |
178178
| Risk if relaxed | L4-only endpoints (no `protocol` field) allow the agent to send any data through the tunnel after the initial connection is permitted. The proxy cannot see or filter the HTTP method, path, or body. The `access: full` preset with `protocol: rest` enables inspection but allows all methods and paths, so it does not restrict what the agent can do at the HTTP level. |
179-
| Recommendation | Use `protocol: rest` with specific `rules` for REST APIs where you want method and path control. Use `protocol: rest` with `access: read-only` for read-only endpoints. Omit `protocol` only for non-HTTP protocols (WebSocket, gRPC streaming) or endpoints that do not need HTTP inspection. |
179+
| Recommendation | Use `protocol: rest` with specific `rules` for REST APIs where you want method and path control. Use `protocol: rest` with `access: read-only` for read-only endpoints. Omit `protocol` only for non-HTTP protocols (WebSocket, gRPC streaming), endpoints that do not need HTTP inspection, or documented compatibility exceptions that require a client-managed CONNECT tunnel. |
180180

181181
### Operator Approval Flow
182182

@@ -201,7 +201,7 @@ NemoClaw ships preset policy files in `nemoclaw-blueprint/policies/presets/` for
201201
| `github` | GitHub and GitHub REST API. | Gives agent read/write access to repositories and issues via `gh` and `git`. |
202202
| `huggingface` | Hugging Face Hub (download-only) and inference router. | Allows downloading arbitrary models and datasets. POST is restricted to the inference router only. |
203203
| `jira` | Atlassian Jira API. | Gives agent read/write access to project issues and comments. |
204-
| `npm` | npm and Yarn registries (GET-only). | Allows installing arbitrary npm packages, which may contain malicious code. Publishing is blocked. |
204+
| `npm` | npm and Yarn registries via L4 pass-through. | Allows installing arbitrary npm packages, which may contain malicious code. OpenShell still gates by host, port, and binary, but does not inspect HTTP method, path, or body for this preset. |
205205
| `outlook` | Microsoft 365, Outlook. | Gives agent access to email. |
206206
| `pypi` | Python Package Index (GET and HEAD only). | Allows installing arbitrary Python packages, which may contain malicious code. Publishing is blocked. |
207207
| `slack` | Slack API, Socket Mode, webhooks. | WebSocket uses `access: full`. Agent can post to any channel the bot token has access to. |
@@ -533,7 +533,7 @@ The following patterns weaken security without providing meaningful benefit.
533533

534534
| Mistake | Why it matters | What to do instead |
535535
|---------|---------------|-------------------|
536-
| Omitting `protocol: rest` on REST API endpoints | Endpoints without a `protocol` field use L4-only enforcement. The proxy allows the TCP stream through after checking host, port, and binary, but cannot see or filter individual HTTP requests. | Add `protocol: rest` with explicit `rules` to enable per-request method and path control on REST APIs. |
536+
| Omitting `protocol: rest` on REST API endpoints without a compatibility reason | Endpoints without a `protocol` field use L4-only enforcement. The proxy allows the TCP stream through after checking host, port, and binary, but cannot see or filter individual HTTP requests. | Add `protocol: rest` with explicit `rules` to enable per-request method and path control on REST APIs. Use L4 pass-through only for documented cases such as npm/Yarn on Node 22, where the client requires a CONNECT tunnel that L7 inspection would break. |
537537
| Adding endpoints to the baseline policy for one-off requests | Adding an endpoint to the baseline policy makes it permanently reachable across all sandbox instances. | Use operator approval. Approved endpoints persist within the sandbox instance but reset when you destroy and recreate the sandbox. |
538538
| Relying solely on the entrypoint for capability drops | The entrypoint drops dangerous capabilities using `capsh`, but this is best-effort. If `capsh` is unavailable or `CAP_SETPCAP` is not in the bounding set, the container runs with the default capability set. | Pass `--cap-drop=ALL` at the container runtime level as defense-in-depth. |
539539
| Leaving `/sandbox/.openclaw` writable on sensitive workloads | This directory contains the OpenClaw gateway configuration. A writable `.openclaw` lets the agent disable CORS, redirect inference routing, or weaken gateway protections. | Run `nemoclaw <name> shields up` to lock config for always-on assistants handling sensitive data. |

nemoclaw-blueprint/policies/presets/npm.yaml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,18 @@ network_policies:
99
npm_yarn:
1010
name: npm_yarn
1111
endpoints:
12+
# L4 CONNECT tunnel. Node 22 undici issues HTTP CONNECT through
13+
# HTTPS_PROXY for TLS tunneling; L7 REST-mode method inspection
14+
# rejects CONNECT and causes ECONNRESET on tarball downloads
15+
# (see #2767). L4 pass-through restores undici compatibility.
1216
- host: registry.npmjs.org
1317
port: 443
14-
protocol: rest
15-
enforcement: enforce
16-
rules:
17-
- allow: { method: GET, path: "/**" }
18+
access: full
19+
tls: skip
1820
- host: registry.yarnpkg.com
1921
port: 443
20-
protocol: rest
21-
enforcement: enforce
22-
rules:
23-
- allow: { method: GET, path: "/**" }
22+
access: full
23+
tls: skip
2424
binaries:
2525
- { path: /usr/local/bin/npm* }
2626
- { path: /usr/local/bin/npx* }

test/policies.test.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -679,22 +679,34 @@ describe("policies", () => {
679679
}
680680
});
681681

682-
it("package-manager presets use protocol: rest with read-only rules", () => {
683-
// Package managers only need read access to install packages.
684-
// Using access: full opens a raw CONNECT tunnel that allows
685-
// PUT/POST (publish, exfiltrate). Restrict via rest rules.
686-
const packagePresets = ["pypi", "npm"];
687-
for (const name of packagePresets) {
688-
const content = requirePresetContent(policies.loadPreset(name));
689-
expect(content).toBeTruthy();
690-
expect(content.includes("access: full")).toBe(false);
691-
expect(content.includes("protocol: rest")).toBe(true);
692-
expect(content.includes("method: GET")).toBe(true);
693-
// No write methods allowed
694-
expect(content.includes("method: PUT")).toBe(false);
695-
expect(content.includes("method: POST")).toBe(false);
696-
expect(content.includes("method: DELETE")).toBe(false);
697-
}
682+
it("pypi preset uses protocol: rest with read-only rules", () => {
683+
// PyPI only needs read access to install packages.
684+
// PyPI's pip uses http.request() (not undici), so it goes through
685+
// http-proxy-fix.js which rewrites FORWARD-mode to https.request,
686+
// avoiding CONNECT entirely. protocol: rest is therefore safe and
687+
// preferred for tighter L7 method enforcement.
688+
const content = requirePresetContent(policies.loadPreset("pypi"));
689+
expect(content).toBeTruthy();
690+
expect(content.includes("access: full")).toBe(false);
691+
expect(content.includes("protocol: rest")).toBe(true);
692+
expect(content.includes("method: GET")).toBe(true);
693+
// No write methods allowed
694+
expect(content.includes("method: PUT")).toBe(false);
695+
expect(content.includes("method: POST")).toBe(false);
696+
expect(content.includes("method: DELETE")).toBe(false);
697+
});
698+
699+
it("npm preset uses L4 tunnel for CONNECT compatibility (#2767)", () => {
700+
// npm on Node 22 uses undici's built-in fetch which bypasses
701+
// http.request() and issues CONNECT directly through HTTPS_PROXY.
702+
// protocol: rest triggers L7 method inspection that rejects
703+
// CONNECT, causing ECONNRESET on tarball downloads. access: full
704+
// with tls: skip uses L4 tunneling that supports CONNECT.
705+
const content = requirePresetContent(policies.loadPreset("npm"));
706+
expect(content).toBeTruthy();
707+
expect(content.includes("access: full")).toBe(true);
708+
expect(content.includes("tls: skip")).toBe(true);
709+
expect(content.includes("protocol: rest")).toBe(false);
698710
});
699711

700712
it("outlook preset allows PATCH on graph.microsoft.com", () => {

test/validate-blueprint.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type Endpoint = {
4545
protocol?: string;
4646
enforcement?: string;
4747
access?: string;
48+
tls?: string;
4849
rules?: Rule[];
4950
binaries?: Array<{ path: string }>;
5051
};
@@ -395,3 +396,40 @@ describe("huggingface preset", () => {
395396
}
396397
});
397398
});
399+
400+
describe("npm preset", () => {
401+
// Regression #2767: npm/Yarn registry endpoints used `protocol: rest`
402+
// with only GET allowed. Node 22 undici issues HTTP CONNECT through
403+
// HTTPS_PROXY for TLS tunneling; the L7 proxy rejects parallel CONNECT
404+
// tunnels, causing NET:FAIL and ECONNRESET on tarball downloads.
405+
// The fix switches to L4 tunnel mode.
406+
const NPM_PRESET_PATH = new URL(
407+
"../nemoclaw-blueprint/policies/presets/npm.yaml",
408+
import.meta.url,
409+
);
410+
const npmPreset = loadYaml<PolicyPreset>(NPM_PRESET_PATH);
411+
412+
function npmEndpoints(): Endpoint[] {
413+
const np = npmPreset.network_policies;
414+
if (!np) return [];
415+
const entry = np.npm_yarn;
416+
return Array.isArray(entry?.endpoints) ? entry.endpoints : [];
417+
}
418+
419+
const REGISTRY_HOSTS = ["registry.npmjs.org", "registry.yarnpkg.com"];
420+
421+
for (const host of REGISTRY_HOSTS) {
422+
it(`regression #2767: ${host} uses L4 tunnel (access: full, tls: skip) for CONNECT compatibility`, () => {
423+
const endpoints = npmEndpoints().filter((ep) => ep.host === host);
424+
expect(endpoints.length).toBeGreaterThan(0);
425+
for (const ep of endpoints) {
426+
expect(ep.access).toBe("full");
427+
expect(ep).toHaveProperty("tls", "skip");
428+
// Must NOT use protocol: rest — that triggers L7 method inspection
429+
// which rejects CONNECT tunnels from Node 22 undici.
430+
expect(ep).not.toHaveProperty("protocol");
431+
expect(ep).not.toHaveProperty("rules");
432+
}
433+
});
434+
}
435+
});

0 commit comments

Comments
 (0)