diff --git a/k8s/README.md b/k8s/README.md index be1a262486b..183c8f18cab 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -2,6 +2,14 @@ > **⚠️ Experimental**: This deployment method is intended for **trying out NemoClaw on Kubernetes**, not for production use. It requires a **privileged pod** running **Docker-in-Docker (DinD)** to create isolated sandbox environments. Operational requirements (storage, runtime, security policies) vary by cluster configuration. +The sample manifest now uses a few safer defaults out of the box: + +- disables Kubernetes service account token automounting +- disables service-link environment injection +- runs the workspace container with `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, and `RuntimeDefault` seccomp +- applies NemoClaw's suggested policy presets instead of skipping policy setup +- downloads the installer to a local file with HTTPS-only curl flags before execution + Run [NemoClaw](https://github.com/NVIDIA/NemoClaw) on Kubernetes with GPU inference powered by [Dynamo](https://github.com/ai-dynamo/dynamo) or any OpenAI-compatible endpoint. --- @@ -17,8 +25,16 @@ Run [NemoClaw](https://github.com/NVIDIA/NemoClaw) on Kubernetes with GPU infere ### 1. Deploy NemoClaw +If your compatible endpoint requires an API key, create the optional +`nemoclaw-compatible-api-key` Secret after creating the namespace and before +running `kubectl apply`. The same Secret-backed flow is described again in the +configuration section below. + ```bash kubectl create namespace nemoclaw +kubectl create secret generic nemoclaw-compatible-api-key \ + -n nemoclaw \ + --from-literal=api-key='' kubectl apply -f https://raw.githubusercontent.com/NVIDIA/NemoClaw/main/k8s/nemoclaw-k8s.yaml ``` @@ -48,9 +64,21 @@ Edit the environment variables in `nemoclaw-k8s.yaml` before deploying: |----------|----------|-------------| | `DYNAMO_HOST` | Yes | Inference endpoint for socat proxy (e.g., `vllm-frontend.dynamo.svc:8000`) | | `NEMOCLAW_ENDPOINT_URL` | Yes | URL the sandbox uses (usually `http://host.openshell.internal:8000/v1`) | -| `COMPATIBLE_API_KEY` | Yes | API key (use `dummy` for Dynamo/vLLM) | +| `COMPATIBLE_API_KEY` | No | Loaded from the optional `nemoclaw-compatible-api-key` Secret; defaults to `dummy` for Dynamo/vLLM when the Secret is absent | | `NEMOCLAW_MODEL` | Yes | Model name (e.g., `meta-llama/Llama-3.1-8B-Instruct`) | | `NEMOCLAW_SANDBOX_NAME` | No | Sandbox name (default: `my-assistant`) | +| `NEMOCLAW_POLICY_MODE` | No | Policy preset mode for non-interactive onboarding (default: `suggested`) | + +### Optional: Store a Real API Key in a Secret + +If your compatible endpoint requires authentication, create the Secret before +you apply the manifest in Step 1: + +```bash +kubectl create secret generic nemoclaw-compatible-api-key \ + -n nemoclaw \ + --from-literal=api-key='' +``` ### Example: Custom Endpoint @@ -60,8 +88,6 @@ env: value: "my-vllm.my-namespace.svc.cluster.local:8000" - name: NEMOCLAW_ENDPOINT_URL value: "http://host.openshell.internal:8000/v1" - - name: COMPATIBLE_API_KEY - value: "dummy" - name: NEMOCLAW_MODEL value: "mistralai/Mistral-7B-Instruct-v0.3" ``` diff --git a/k8s/nemoclaw-k8s.yaml b/k8s/nemoclaw-k8s.yaml index 3865f8b983b..53fe59ac5c9 100644 --- a/k8s/nemoclaw-k8s.yaml +++ b/k8s/nemoclaw-k8s.yaml @@ -9,6 +9,8 @@ metadata: labels: app: nemoclaw spec: + automountServiceAccountToken: false + enableServiceLinks: false containers: # Docker daemon (DinD) - name: dind @@ -34,6 +36,13 @@ spec: # Workspace - runs official NemoClaw installer - name: workspace image: node:22 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault command: - bash - -c @@ -61,9 +70,20 @@ spec: docker info >/dev/null 2>&1 || { echo "Docker not ready"; exit 1; } echo "Docker ready" + # Default to a dummy compatible API key for unauthenticated endpoints + # such as Dynamo/vLLM while still allowing a Secret-backed override. + export COMPATIBLE_API_KEY="${COMPATIBLE_API_KEY:-dummy}" + # Run official NemoClaw installer echo "[4/4] Running NemoClaw installer..." - curl -fsSL https://nvidia.com/nemoclaw.sh | bash + umask 077 + curl --proto '=https' --tlsv1.2 --fail --show-error --silent \ + --location \ + --output /tmp/nemoclaw-install.sh \ + https://www.nvidia.com/nemoclaw.sh + chmod 700 /tmp/nemoclaw-install.sh + bash /tmp/nemoclaw-install.sh + rm -f /tmp/nemoclaw-install.sh # Keep running after onboard echo "Onboard complete. Container staying alive." @@ -82,13 +102,17 @@ spec: - name: NEMOCLAW_ENDPOINT_URL value: "http://host.openshell.internal:8000/v1" - name: COMPATIBLE_API_KEY - value: "dummy" + valueFrom: + secretKeyRef: + name: nemoclaw-compatible-api-key + key: api-key + optional: true - name: NEMOCLAW_MODEL value: "meta-llama/Llama-3.1-8B-Instruct" - name: NEMOCLAW_SANDBOX_NAME value: "my-assistant" - name: NEMOCLAW_POLICY_MODE - value: "skip" + value: "suggested" - name: NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE value: "1" volumeMounts: diff --git a/test/security-configuration-hardening.test.js b/test/security-configuration-hardening.test.js new file mode 100644 index 00000000000..e87c6f44f11 --- /dev/null +++ b/test/security-configuration-hardening.test.js @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it, expect } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; + +const ROOT = path.join(import.meta.dirname, ".."); +const K8S_MANIFEST = path.join(ROOT, "k8s", "nemoclaw-k8s.yaml"); + +describe("security configuration hardening", () => { + it("hardens the Kubernetes sample manifest with safer defaults", () => { + const manifest = fs.readFileSync(K8S_MANIFEST, "utf8"); + const workspaceMatch = manifest.match( + /- name: workspace[\s\S]*?(?=\n\s*-\s*name: |\n\s*initContainers:|\n\s*volumes:|$)/, + ); + expect(workspaceMatch).not.toBeNull(); + const workspaceSection = workspaceMatch[0]; + expect(manifest).toMatch(/automountServiceAccountToken:\s*false/); + expect(manifest).toMatch(/enableServiceLinks:\s*false/); + expect(workspaceSection).toMatch(/allowPrivilegeEscalation:\s*false/); + expect(workspaceSection).toMatch(/capabilities:\s*[\r\n]+\s*drop:\s*[\r\n]+\s*-\s*ALL/); + expect(workspaceSection).toMatch(/seccompProfile:\s*[\r\n]+\s*type:\s*RuntimeDefault/); + expect(manifest).toMatch(/- name: NEMOCLAW_POLICY_MODE[\s\S]*value:\s*"suggested"/); + expect(manifest).toContain('export COMPATIBLE_API_KEY="${COMPATIBLE_API_KEY:-dummy}"'); + const compatibleApiKeySection = manifest.match( + /- name: COMPATIBLE_API_KEY[\s\S]*?(?=\n\s*-\s*name: |\n\s*volumeMounts:|\n\s*command:|$)/, + )?.[0]; + expect(compatibleApiKeySection).toBeTruthy(); + expect(compatibleApiKeySection).toMatch( + /secretKeyRef:[\s\S]*name:\s*nemoclaw-compatible-api-key/, + ); + expect(compatibleApiKeySection).toMatch(/optional:\s*true/); + expect(manifest).toContain("curl --proto '=https' --tlsv1.2 --fail --show-error --silent"); + expect(manifest).toContain("--output /tmp/nemoclaw-install.sh"); + expect(manifest).toContain("chmod 700 /tmp/nemoclaw-install.sh"); + expect(manifest).toContain("bash /tmp/nemoclaw-install.sh"); + expect(manifest).not.toMatch(/curl\b[^\n|]*\|\s*(?:ba|z|k)?sh\b/i); + }); +});