diff --git a/docs/index.md b/docs/index.md index 5d6adc91d09..57d08c4bbd5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -261,6 +261,7 @@ Customize the Network Policy :hidden: Security Best Practices +Credential Storage ``` ```{toctree} diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md index 4852806ea6d..0c516b6eb5b 100644 --- a/docs/reference/architecture.md +++ b/docs/reference/architecture.md @@ -178,7 +178,7 @@ NemoClaw keeps its operator-facing state on the host rather than inside the sand | Path | Purpose | |---|---| -| `~/.nemoclaw/credentials.json` | Provider credentials saved during onboarding. | +| `~/.nemoclaw/credentials.json` | Provider credentials saved during onboarding. Stored as plaintext JSON protected by local filesystem permissions; see [Credential Storage](../security/credential-storage.md). | | `~/.nemoclaw/sandboxes.json` | Registered sandbox metadata, including the default sandbox selection. | | `~/.openclaw/openclaw.json` | Host OpenClaw configuration that NemoClaw snapshots or restores during migration flows. | diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 4f2c39dec9d..81050adc914 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -73,7 +73,7 @@ Avoid `openshell self-update`, `npm update -g openshell`, `openshell gateway sta The wizard prompts for a provider first, then collects the provider credential if needed. Supported non-experimental choices include NVIDIA Endpoints, OpenAI, Anthropic, Google Gemini, and compatible OpenAI or Anthropic endpoints. -Credentials are stored in `~/.nemoclaw/credentials.json`. +Credentials are stored in `~/.nemoclaw/credentials.json`. For file permissions, plaintext storage behavior, and hardening guidance, see [Credential Storage](../security/credential-storage.md). The legacy `nemoclaw setup` command is deprecated; use `nemoclaw onboard` instead. If you enable Brave Search during onboarding, NemoClaw currently stores the Brave API key in the sandbox's OpenClaw configuration. diff --git a/docs/security/credential-storage.md b/docs/security/credential-storage.md new file mode 100644 index 00000000000..b8d55def72d --- /dev/null +++ b/docs/security/credential-storage.md @@ -0,0 +1,133 @@ +--- +title: + page: "NemoClaw Credential Storage" + nav: "Credential Storage" +description: + main: "Learn where NemoClaw stores credentials, what filesystem protections it applies, and how to secure or rotate stored secrets." + agent: "Explains where NemoClaw stores provider credentials, the file permissions it applies, and the operational security trade-offs of plaintext local storage. Use when reviewing credential handling or advising users how to secure stored API keys." +keywords: ["nemoclaw credential storage", "credentials.json", "api key security"] +topics: ["generative_ai", "ai_agents"] +tags: ["security", "credentials", "filesystem", "secrets"] +content: + type: reference + difficulty: technical_beginner + audience: ["developer", "engineer"] +status: published +--- + + + +# Credential Storage + +NemoClaw stores operator-provided host-side credentials under `~/.nemoclaw/`. +These credentials are used during onboarding and host-side lifecycle operations. +They are not encrypted at rest by NemoClaw. +Instead, NemoClaw relies on local filesystem ownership and Unix permissions to limit access. + +## Location and Permissions + +By default, NemoClaw stores credentials in: + +```text +~/.nemoclaw/credentials.json +``` + +When NemoClaw creates this state directory, it uses owner-only permissions: + +- `~/.nemoclaw/` is created with mode `0700` +- `~/.nemoclaw/credentials.json` is written with mode `0600` + +That means only the local account that owns the files should be able to read or modify them. + +NemoClaw also refuses to use obviously unsafe `HOME` paths such as `/tmp`, `/var/tmp`, `/dev/shm`, or `/` for credential storage. +If `HOME` points to one of those locations, onboarding exits with an error instead of writing secrets there. + +## Plaintext Storage Warning + +The credential file is plaintext JSON. +NemoClaw does **not** currently encrypt the file or integrate with the host operating system keychain. + +A typical file looks like this: + +```json +{ + "NVIDIA_API_KEY": "nvapi-...", + "GITHUB_TOKEN": "ghp_...", + "OPENAI_API_KEY": "sk-..." +} +``` + +Treat this file like any other local secret material. +Anyone who can read it can reuse those credentials with the upstream provider. + +## Precedence and Scope + +When NemoClaw looks up a credential, it checks environment variables first. +If the corresponding environment variable is set, NemoClaw uses that value instead of the stored file. + +This behavior is useful for: + +- CI or automation where you do not want to persist secrets to disk +- temporary overrides during testing +- short-lived or rotated credentials + +For interactive local use, `nemoclaw onboard` can save credentials into `~/.nemoclaw/credentials.json` so future runs do not prompt again. + +## Security Recommendations + +Use the following practices to reduce the risk of credential exposure. + +1. Keep your home directory private and owned by your user account. +2. Exclude `~/.nemoclaw/` from cloud-sync folders, shared folders, and broad backup exports unless those systems are already approved for secret storage. +3. Prefer short-lived or low-scope provider credentials where the upstream service supports them. +4. Rotate keys after suspected exposure, machine transfer, or account changes. +5. Prefer environment variables for ephemeral automation instead of persisting long-lived secrets locally. +6. Do not copy `credentials.json` into container images, Git repositories, bug reports, or support bundles. + +## Inspect and Repair Permissions + +To inspect the current permissions: + +```console +$ ls -ld ~/.nemoclaw ~/.nemoclaw/credentials.json +``` + +Expected output should show a private directory and file, for example: + +```text +drwx------ ... ~/.nemoclaw +-rw------- ... ~/.nemoclaw/credentials.json +``` + +If the permissions are broader than expected, tighten them: + +```console +$ chmod 700 ~/.nemoclaw +$ chmod 600 ~/.nemoclaw/credentials.json +``` + +## Rotate or Remove Stored Credentials + +The simplest way to replace a stored provider key is to rerun onboarding and provide the new value when prompted: + +```console +$ nemoclaw onboard +``` + +To remove the stored file entirely: + +```console +$ rm -f ~/.nemoclaw/credentials.json +``` + +On the next run, NemoClaw prompts again unless the credential is supplied through the environment. + +## Related Files + +Other NemoClaw host-side state also lives under `~/.nemoclaw/`, such as sandbox registry metadata. +These files are operational state, not provider secrets, but they should still remain in a user-owned home directory. + +For the broader sandbox security model and operational trade-offs, see [Security Best Practices](./best-practices.md) and [Architecture](../reference/architecture.md). diff --git a/test/credentials.test.js b/test/credentials.test.js index 8b122c40946..e66dcb265e2 100644 --- a/test/credentials.test.js +++ b/test/credentials.test.js @@ -43,6 +43,11 @@ describe("credential prompts", () => { fs.readFileSync(path.join(home, ".nemoclaw", "credentials.json"), "utf-8"), ); expect(saved).toEqual({ TEST_API_KEY: "nvapi-saved-key" }); + + const dirMode = fs.statSync(path.join(home, ".nemoclaw")).mode & 0o777; + const fileMode = fs.statSync(path.join(home, ".nemoclaw", "credentials.json")).mode & 0o777; + expect(dirMode).toBe(0o700); + expect(fileMode).toBe(0o600); }); it("prefers environment credentials and ignores malformed credential files", async () => {