fix: remove gcc and netcat from sandbox image (#807, #808)#810
fix: remove gcc and netcat from sandbox image (#807, #808)#810cluster2600 wants to merge 4 commits into
Conversation
|
Caution Review failedPull request was closed or merged during review Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (3)
📝 WalkthroughWalkthroughThe changes implement fork-bomb protection and sandbox hardening by adding documentation for process limit enforcement at two layers (container and in-container), removing unnecessary packages (gcc, netcat) from the sandbox image, and introducing tests to verify the hardening measures. Changes
Sequence Diagram(s)sequenceDiagram
actor Operator
participant ContainerRuntime as Container Runtime<br/>(nemoclaw onboard)
participant Kernel as Kernel<br/>cgroup (pids.max)
participant Container as Container<br/>Startup
participant Shell as Shell Script<br/>(nemoclaw-start.sh)
Operator->>ContainerRuntime: Deploy NemoClaw
ContainerRuntime->>ContainerRuntime: docker update --pids-limit 512
ContainerRuntime->>Kernel: Set cgroup pids.max = 512
Operator->>Container: Start container
Container->>Shell: Execute nemoclaw-start.sh
Shell->>Shell: ulimit -u (query current limit)
alt Limit is unlimited
Shell->>Shell: ulimit -u 512 (attempt to set)
Shell->>Kernel: Register process limit
else Limit already set
Shell->>Shell: Skip override
end
Shell->>Shell: Continue startup
note over Kernel: Fork attempts beyond 512<br/>fail with EAGAIN
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
docs/security/process-limits.md (1)
33-34: One sentence per line for readable diffs.Per documentation guidelines, each sentence should be on its own line in the source.
📝 Proposed fix
-The default process limit is 512 per sandbox. This is sufficient for normal -agent operation (typically < 50 processes) whilst preventing fork bombs. +The default process limit is 512 per sandbox. +This is sufficient for normal agent operation (typically < 50 processes) whilst preventing fork bombs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/security/process-limits.md` around lines 33 - 34, Split the two sentences in the paragraph so each sentence occupies its own line in the source: move "The default process limit is 512 per sandbox." and "This is sufficient for normal agent operation (typically < 50 processes) whilst preventing fork bombs." onto separate lines (i.e., one sentence per line) to follow the documentation guideline for readable diffs.docs/security/sandbox-hardening.md (1)
41-43: One sentence per line for readable diffs.Per documentation guidelines, each sentence should be on its own line.
📝 Proposed fix
-Even if one layer is bypassed, the others provide protection. Removing tools -from the image means an attacker who escapes the proxy still cannot compile -custom tooling or open raw TCP connections. +Even if one layer is bypassed, the others provide protection. +Removing tools from the image means an attacker who escapes the proxy still cannot compile custom tooling or open raw TCP connections.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/security/sandbox-hardening.md` around lines 41 - 43, The paragraph "Even if one layer is bypassed, the others provide protection. Removing tools from the image means an attacker who escapes the proxy still cannot compile custom tooling or open raw TCP connections." should be split so each sentence is on its own line; edit the docs/security/sandbox-hardening.md content replacing that single-line paragraph with two lines, one containing "Even if one layer is bypassed, the others provide protection." and the next containing "Removing tools from the image means an attacker who escapes the proxy still cannot compile custom tooling or open raw TCP connections." to satisfy the one-sentence-per-line guideline.test/process-limits.test.js (1)
66-69: Make the default-value assertion resilient to equivalent refactors.The regex at Line 68 only accepts
|| "512". If code switches to nullish coalescing (?? "512"), behavior stays correct but this test fails. Consider allowing both operators.Optional refactor
- expect(onboardJs).toMatch(/\|\|\s*"512"/); + expect(onboardJs).toMatch(/(\|\||\?\?)\s*["']512["']/);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/process-limits.test.js` around lines 66 - 69, Update the assertion in the test "uses NEMOCLAW_PIDS_LIMIT env var with 512 default" to accept both logical OR and nullish coalescing forms; modify the expect(onboardJs).toMatch(...) regex to allow either "||" or "??" (with optional whitespace) before the default string "512" so it still passes if code switches from || "512" to ?? "512".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@bin/lib/onboard.js`:
- Around line 637-641: The pidsLimit environment value (NEMOCLAW_PIDS_LIMIT) is
injected directly into the shell command used by runCapture which allows command
injection; validate and sanitize it before use by parsing it as an integer and
falling back to a safe default if invalid, then pass that numeric value (e.g.,
parsedPidLimit) into the docker update invocation instead of the raw string;
update code references where pidsLimit is read and used (NEMOCLAW_PIDS_LIMIT,
pidsLimit, runCapture, and the docker update command that uses sandboxName) to
enforce an integer-only range (e.g., positive integer and reasonable upper
bound) and handle errors by logging and using the default.
In `@docs/security/process-limits.md`:
- Around line 62-64: Change the CLI code block to use the console language tag
and add a $ prompt: replace the ```bash fence containing
"NEMOCLAW_PIDS_LIMIT=1024 nemoclaw onboard" with a ```console fence and prefix
the command with "$ " so the block reads "$ NEMOCLAW_PIDS_LIMIT=1024 nemoclaw
onboard"; update the code fence start/end accordingly around the
"NEMOCLAW_PIDS_LIMIT" environment variable and "nemoclaw onboard" command.
- Around line 1-4: Add YAML frontmatter to the top of the page containing title,
description, keywords, topics, tags, content_type (or content type), difficulty,
audience, and status fields, then immediately after the frontmatter add the
required SPDX license header (e.g., "SPDX-License-Identifier: <license>");
ensure the frontmatter values are concise and accurate for "Process Limits" /
"Fork Bomb Protection" and include relevant keywords and topics so the page
meets the documentation guidelines.
In `@docs/security/sandbox-hardening.md`:
- Around line 1-6: The document lacks the required SPDX license header and
frontmatter and uses passive voice; add the standard SPDX header line at the top
and a YAML frontmatter block containing required fields (title, description,
author/date or other repo-required keys) before the content, and rewrite passive
sentences like "The NemoClaw sandbox image is hardened..." to active voice
(e.g., "NemoClaw hardens the sandbox image...")—update the title "Sandbox Image
Hardening" and the sentence mentioning "NemoClaw sandbox image" to reflect
active voice and ensure frontmatter values match those names.
---
Nitpick comments:
In `@docs/security/process-limits.md`:
- Around line 33-34: Split the two sentences in the paragraph so each sentence
occupies its own line in the source: move "The default process limit is 512 per
sandbox." and "This is sufficient for normal agent operation (typically < 50
processes) whilst preventing fork bombs." onto separate lines (i.e., one
sentence per line) to follow the documentation guideline for readable diffs.
In `@docs/security/sandbox-hardening.md`:
- Around line 41-43: The paragraph "Even if one layer is bypassed, the others
provide protection. Removing tools from the image means an attacker who escapes
the proxy still cannot compile custom tooling or open raw TCP connections."
should be split so each sentence is on its own line; edit the
docs/security/sandbox-hardening.md content replacing that single-line paragraph
with two lines, one containing "Even if one layer is bypassed, the others
provide protection." and the next containing "Removing tools from the image
means an attacker who escapes the proxy still cannot compile custom tooling or
open raw TCP connections." to satisfy the one-sentence-per-line guideline.
In `@test/process-limits.test.js`:
- Around line 66-69: Update the assertion in the test "uses NEMOCLAW_PIDS_LIMIT
env var with 512 default" to accept both logical OR and nullish coalescing
forms; modify the expect(onboardJs).toMatch(...) regex to allow either "||" or
"??" (with optional whitespace) before the default string "512" so it still
passes if code switches from || "512" to ?? "512".
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3c7f63b2-2a81-4752-a165-02e4108ce379
📒 Files selected for processing (9)
Dockerfilebin/lib/onboard.jsdocs/security/process-limits.mddocs/security/sandbox-hardening.mdnemoclaw-blueprint/policies/openclaw-sandbox.yamlscripts/nemoclaw-start.shtest/Dockerfile.sandboxtest/dockerfile-attack-surface.test.jstest/process-limits.test.js
|
✨ Thanks for submitting this PR with a detailed summary, it addresses a security concern and improves the overall security of the sandbox image, which could reduce the risk of vulnerabilities. |
2716869 to
f061384
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
bin/lib/onboard.js (1)
1511-1515:⚠️ Potential issue | 🔴 CriticalSanitize
NEMOCLAW_PIDS_LIMITto prevent command injection.The
pidsLimitvalue is interpolated directly into the shell command without validation. A malicious environment variable likeNEMOCLAW_PIDS_LIMIT="512; rm -rf /"could execute arbitrary commands.Note: This was flagged in a previous review where the author indicated the code was in PR
#811, but the vulnerability exists in this PR's code as well.🛡️ Proposed fix to validate the pids limit
const pidsLimit = process.env.NEMOCLAW_PIDS_LIMIT || "512"; + if (!/^\d+$/.test(pidsLimit)) { + console.error(` Invalid NEMOCLAW_PIDS_LIMIT: ${pidsLimit} (must be a positive integer)`); + process.exit(1); + } const dockerUpdate = runCapture( `docker update --pids-limit ${pidsLimit} "${sandboxName}" 2>&1`, { ignoreError: true } );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@bin/lib/onboard.js` around lines 1511 - 1515, pidsLimit is taken directly from process.env and interpolated into the shell command, allowing command injection; change the logic around the pidsLimit assignment so you parse and validate it as an integer (e.g., const parsed = parseInt(process.env.NEMOCLAW_PIDS_LIMIT, 10)), ensure it is a finite number within an acceptable range (fallback to 512 if NaN/out-of-range), and then use that validated integer when calling runCapture for the `docker update --pids-limit ${pidsLimit}` invocation so only a numeric value is injected into the command.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Dockerfile`:
- Around line 31-35: The Dockerfile RUN step that does "apt-get update &&
apt-get purge -y --auto-remove ... && rm -rf /var/lib/apt/lists/* || true" is
masking failures; remove the trailing "|| true" so the build fails if apt-get
update/purge/cleanup fails, ensuring the purge of packages like gcc/g++/make and
netcat completes or the build stops; keep the existing chained commands and
error propagation semantics (no additional try/catch), so failures in that RUN
line fail the image build.
---
Duplicate comments:
In `@bin/lib/onboard.js`:
- Around line 1511-1515: pidsLimit is taken directly from process.env and
interpolated into the shell command, allowing command injection; change the
logic around the pidsLimit assignment so you parse and validate it as an integer
(e.g., const parsed = parseInt(process.env.NEMOCLAW_PIDS_LIMIT, 10)), ensure it
is a finite number within an acceptable range (fallback to 512 if
NaN/out-of-range), and then use that validated integer when calling runCapture
for the `docker update --pids-limit ${pidsLimit}` invocation so only a numeric
value is injected into the command.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2272595c-a1c3-4561-b8b8-605dc422d662
📒 Files selected for processing (8)
Dockerfilebin/lib/onboard.jsdocs/security/process-limits.mddocs/security/sandbox-hardening.mdnemoclaw-blueprint/policies/openclaw-sandbox.yamlscripts/nemoclaw-start.shtest/dockerfile-attack-surface.test.jstest/process-limits.test.js
✅ Files skipped from review due to trivial changes (5)
- nemoclaw-blueprint/policies/openclaw-sandbox.yaml
- test/process-limits.test.js
- docs/security/process-limits.md
- docs/security/sandbox-hardening.md
- scripts/nemoclaw-start.sh
🚧 Files skipped from review as they are similar to previous changes (1)
- test/dockerfile-attack-surface.test.js
| RUN apt-get update && apt-get purge -y --auto-remove \ | ||
| gcc g++ cpp make \ | ||
| netcat-openbsd netcat-traditional \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| || true |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether a hardening purge command is currently error-masked.
set -euo pipefail
rg -nP 'apt-get\s+purge[\s\S]*\|\|\s*true' Dockerfile -C 2
# Expected: no matches after fix.Repository: NVIDIA/NemoClaw
Length of output: 41
🏁 Script executed:
#!/bin/bash
# Directly read the Dockerfile around the specified lines
cat -n Dockerfile | sed -n '25,40p'Repository: NVIDIA/NemoClaw
Length of output: 879
Remove || true from the security-hardening purge step.
Lines 31–35 contain || true, which suppresses failures from apt-get update, apt-get purge, and cleanup operations. If any of these commands fail, the build succeeds anyway while dangerous packages remain installed, undermining the security guarantee documented in the surrounding comments.
Suggested fix
RUN apt-get update && apt-get purge -y --auto-remove \
gcc g++ cpp make \
netcat-openbsd netcat-traditional \
&& rm -rf /var/lib/apt/lists/* \
- || true📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| RUN apt-get update && apt-get purge -y --auto-remove \ | |
| gcc g++ cpp make \ | |
| netcat-openbsd netcat-traditional \ | |
| && rm -rf /var/lib/apt/lists/* \ | |
| || true | |
| RUN apt-get update && apt-get purge -y --auto-remove \ | |
| gcc g++ cpp make \ | |
| netcat-openbsd netcat-traditional \ | |
| && rm -rf /var/lib/apt/lists/* |
🧰 Tools
🪛 Trivy (0.69.3)
[error] 31-35: 'RUN update' instruction alone
The instruction 'RUN update' should always be followed by ' install' in the same RUN statement.
Rule: DS-0017
(IaC/Dockerfile)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Dockerfile` around lines 31 - 35, The Dockerfile RUN step that does "apt-get
update && apt-get purge -y --auto-remove ... && rm -rf /var/lib/apt/lists/* ||
true" is masking failures; remove the trailing "|| true" so the build fails if
apt-get update/purge/cleanup fails, ensuring the purge of packages like
gcc/g++/make and netcat completes or the build stops; keep the existing chained
commands and error propagation semantics (no additional try/catch), so failures
in that RUN line fail the image build.
There was a problem hiding this comment.
The || true is intentional — node:22-slim doesn't ship gcc or netcat in all builds, so the purge can fail legitimately on images that don't have them. Removing it would break the build on those base images.
The security guarantee comes from the test (dockerfile-attack-surface.test.js) verifying the purge line is present, plus the runtime verification we ran on the DGX Spark confirming the packages are actually gone.
That said, a tighter approach would be to check before purging:
RUN dpkg -l gcc g++ cpp make netcat-openbsd netcat-traditional 2>/dev/null | grep -q ^ii \
&& apt-get update && apt-get purge -y --auto-remove gcc g++ cpp make netcat-openbsd netcat-traditional \
&& rm -rf /var/lib/apt/lists/* \
|| trueHappy to update if the maintainers prefer that pattern — it makes the || true only cover the "packages not installed" case, not a genuine purge failure.
There was a problem hiding this comment.
@cluster2600, thanks for the clear explanation — you're right. apt-get purge exits with code 100 when a package isn't installed, so || true is necessary for portability across node:22-slim variants that may not include those packages. I withdraw my original concern on that basis.
The dpkg -l pre-check you proposed is a nice improvement in clarity:
RUN dpkg -l gcc g++ cpp make netcat-openbsd netcat-traditional 2>/dev/null | grep -q ^ii \
&& apt-get update && apt-get purge -y --auto-remove gcc g++ cpp make netcat-openbsd netcat-traditional \
&& rm -rf /var/lib/apt/lists/* \
|| trueOne small caveat: because grep -q ^ii exits 0 as soon as it finds any ii-prefixed line, if only a subset of those packages are installed and the purge fails mid-way for a real reason, the trailing || true still masks it. That's a marginal scenario, and the overall intent is clearer than the current form — so either way is fine. The existing test coverage in dockerfile-attack-surface.test.js and the runtime DGX Spark verification are the stronger guarantees here.
(*ᵔ ᗜ ᔕ*)
✏️ Learnings added
Learnt from: cluster2600
Repo: NVIDIA/NemoClaw PR: 810
File: Dockerfile:31-35
Timestamp: 2026-03-25T19:32:46.200Z
Learning: In the NemoClaw `Dockerfile`, the `|| true` appended to the `apt-get purge` hardening step (removing gcc, g++, cpp, make, netcat-openbsd, netcat-traditional) is intentional. `apt-get purge` exits with code 100 when a package is absent, which would break builds on `node:22-slim` variants that don't include those packages. Do not flag this `|| true` as masking a security issue — the guarantee that packages are absent is validated by `test/dockerfile-attack-surface.test.js` and runtime verification.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: cluster2600
Repo: NVIDIA/NemoClaw PR: 782
File: test/config-io.test.js:54-75
Timestamp: 2026-03-24T11:21:32.603Z
Learning: In the NVIDIA/NemoClaw repo, the test suite (including `test/config-io.test.js`) runs on Ubuntu CI runners as a non-root user. Chmod-driven EACCES assertions (e.g., `fs.chmodSync(dir, 0o500)` to simulate permission errors) are intentional and reliable in this environment. Do not flag these as fragile or platform-unsafe unless there is evidence of root/Windows CI usage.
Learnt from: cluster2600
Repo: NVIDIA/NemoClaw PR: 782
File: test/policies.test.js:0-0
Timestamp: 2026-03-24T11:21:33.065Z
Learning: In NVIDIA/NemoClaw, test files (e.g., `test/policies.test.js`) intentionally use raw substring checks (`content.includes("binaries:")`, `content.includes("network_policies:")`, etc.) to validate YAML preset structure rather than parsing YAML. This is a deliberate project convention — no YAML parser dependency is added just for tests. Do not flag these substring assertions as insufficient or recommend switching to structural YAML parsing.
Learnt from: paritoshd-nv
Repo: NVIDIA/NemoClaw PR: 678
File: spark-install.md:105-105
Timestamp: 2026-03-23T01:26:31.640Z
Learning: In `spark-install.md`, the command `docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi` is intentionally correct. The NVIDIA Container Toolkit mounts `nvidia-smi` from the host into any container (including the plain `ubuntu` image) at runtime via the utility capability — no CUDA base image is required. Do not flag this pattern as an issue.
Learnt from: rwipfelnv
Repo: NVIDIA/NemoClaw PR: 227
File: k8s/nemoclaw-k8s.yaml:69-71
Timestamp: 2026-03-25T16:04:51.157Z
Learning: In `k8s/nemoclaw-k8s.yaml` (and any NemoClaw K8s testing manifests), the line `curl -fsSL https://nvidia.com/nemoclaw.sh | bash` is the official documented installation command for NemoClaw as published by NVIDIA at docs.nvidia.com/nemoclaw and nvidia.com/nemoclaw.sh. Do not flag this as a supply-chain or curl|bash security issue — it intentionally mirrors the upstream documented installer invocation.
f061384 to
fa2698f
Compare
Strip compiler toolchain (gcc, g++, cpp, make) and netcat from the runtime image to reduce attack surface. A prompt-injected agent can no longer compile LD_PRELOAD libraries or open reverse shells. Signed-off-by: Maxime Grenu <maxime.grenu@gmail.com>
Set ulimit -u 512 in nemoclaw-start.sh when the container runtime hasn't set a process limit. Documents the need for --pids-limit at the container level as a follow-up. Signed-off-by: Maxime Grenu <maxime.grenu@gmail.com>
Add frontmatter, SPDX licence header, use active voice, and myst-compatible mermaid fencing per documentation guidelines. Signed-off-by: Maxime Grenu <maxime.grenu@gmail.com>
ESLint flags __dirname as undefined in ESM context. Use import.meta.dirname which is the standard pattern in this project. Signed-off-by: Maxime Grenu <maxime.grenu@gmail.com>
fa2698f to
2747d3c
Compare
|
Closing — these changes landed in #830 (remove gcc/netcat, drop capabilities). Cheers to @BenediktSchackenberg for pushing it through. |
Summary
Closes #807, closes #808
Defence layers
graph TD subgraph "Image Hardening" A[Base Image] -->|apt-get purge| B[No gcc/g++/make] A -->|apt-get purge| C[No netcat] B --> D[Cannot compile LD_PRELOAD libraries] C --> E[Cannot open reverse shells] end subgraph "Existing Defences" F[Landlock] --> G[Read-only /usr /etc] H[Network Proxy] --> I[Allowlisted egress] end D --> J[Defence in Depth] E --> J F --> J H --> JTest plan
npx vitest run test/dockerfile-attack-surface.test.jspassesdocker build -t nemoclaw-hardened .succeedswhich gccreturns not foundwhich ncreturns not foundSummary by CodeRabbit
New Features
Documentation
Tests