diff --git a/docs/index.yml b/docs/index.yml index ce7f6b6babd..3a79e8e4d5b 100644 --- a/docs/index.yml +++ b/docs/index.yml @@ -140,6 +140,9 @@ navigation: - page: "Credential Storage" path: _build/agent-variants/security/credential-storage.openclaw.generated.mdx slug: credential-storage + - page: "Credential Rotation" + path: _build/agent-variants/security/credential-rotation.openclaw.generated.mdx + slug: credential-rotation - page: "Trusted Computing Base" path: _build/agent-variants/security/tcb-boundary.openclaw.generated.mdx slug: trusted-computing-base @@ -427,6 +430,9 @@ navigation: - page: "Credential Storage" path: _build/agent-variants/security/credential-storage.hermes.generated.mdx slug: credential-storage + - page: "Credential Rotation" + path: _build/agent-variants/security/credential-rotation.hermes.generated.mdx + slug: credential-rotation - page: "Trusted Computing Base" path: _build/agent-variants/security/tcb-boundary.hermes.generated.mdx slug: trusted-computing-base diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index dfd0a166d64..b25905e06f7 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -1454,10 +1454,19 @@ Do not log it, share it, or commit it to version control. -`gateway-token` is not applicable to Hermes sandboxes. -Hermes API access uses bearer-token authentication configured through the Hermes runtime, not the OpenClaw gateway token. -For browser access to the dashboard, use `nemohermes my-assistant dashboard-url`; Hermes dashboard auth is read from the in-sandbox config (`/sandbox/.hermes/config.yaml`), not a gateway token. -If you need the endpoint for an OpenAI-compatible client, use `nemohermes my-assistant status` and the API URL it reports. +Print the Hermes API bearer token for a running sandbox to stdout. +NemoClaw retrieves the sandbox's `API_SERVER_KEY`, which authenticates OpenAI-compatible clients on the forwarded API port. +Capture the token and pass it in the `Authorization` header: + +```bash +TOKEN=$(nemohermes my-assistant gateway-token --quiet) +curl -fsS -H "Authorization: Bearer $TOKEN" \ + http://127.0.0.1:8642/v1/models +``` + +Treat the token like a password. +Do not log it, share it, or commit it to version control. +For browser access to the dashboard, use `nemohermes my-assistant dashboard-url`. diff --git a/docs/security/credential-rotation.mdx b/docs/security/credential-rotation.mdx new file mode 100644 index 00000000000..4fdd7bae56d --- /dev/null +++ b/docs/security/credential-rotation.mdx @@ -0,0 +1,239 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +title: "Credential Rotation" +sidebar-title: "Credential Rotation" +description: "Rotate inference, messaging, and web search credentials through the supported NemoClaw workflows." +description-agent: "Step-by-step guide for rotating inference API keys, messaging tokens, and web search credentials in NemoClaw. Use when a key expires, is compromised, or must be replaced." +keywords: ["nemoclaw credential rotation", "rotate api key", "update inference key", "messaging token", "nemoclaw credentials reset"] +content: + type: "how_to" +--- +import { AgentOnly } from "../_components/AgentGuide"; + +NemoClaw uses different rotation paths for inference, messaging, and web search credentials. +Inference credentials can normally be updated while reusing the existing sandbox. +Messaging tokens and web search settings require a rebuild or recreation because their configuration is applied when the sandbox image starts. + +## Before You Start + +List the provider names registered with the OpenShell gateway. + +```bash +$$nemoclaw credentials list +``` + +The output is authoritative for commands that accept an OpenShell provider name. +An inference provider is commonly named `nvidia-prod`, while an onboarded web search provider is commonly named `-brave-search` or `-tavily-search`. +Provider names can differ with your selected inference route and sandbox configuration. + +Per-sandbox messaging bridge names are not resettable credentials. +Use `channels add`, `channels remove`, or `channels stop` for messaging integrations instead of passing a bridge name to `credentials reset`. + +| Credential | Supported rotation path | Sandbox impact | +|---|---|---| +| Inference API key | Rerun onboarding with the replacement value | The existing sandbox can normally be reused unless onboarding detects unrelated configuration drift | +| Slack, Telegram, or Discord token | Re-add the channel, then rebuild | Rebuild required | +| Brave or Tavily web search key | Rerun onboarding with the selected web search provider | Sandbox recreation required | + +## Rotate an Inference API Key + +Supply the replacement key and rerun onboarding for the existing sandbox. +Read replacement credentials silently on a trusted host so their values do not enter shell history or terminal scrollback. +Unset each variable after the command finishes. + +```bash +printf 'New NVIDIA inference API key: ' >&2 +IFS= read -r -s NVIDIA_INFERENCE_API_KEY +printf '\n' >&2 +export NVIDIA_INFERENCE_API_KEY +$$nemoclaw onboard --name \ + --non-interactive --yes --yes-i-accept-third-party-software +unset NVIDIA_INFERENCE_API_KEY +``` + +Onboarding updates the selected OpenShell inference provider and reuses the sandbox when its recorded configuration is still compatible. +If onboarding detects other configuration drift, review the requested rebuild or recreation before continuing. + +For an interactive rotation, export the replacement key and run `$$nemoclaw onboard --name ` without the non-interactive flags. + +## Rotate a Messaging Token + +Re-adding an existing channel overwrites its stored credentials. +The replacement takes effect only after a rebuild because messaging providers are resolved when the sandbox starts. + +### Slack + +Slack requires both replacement tokens. + +```bash +printf 'New Slack bot token: ' >&2 +IFS= read -r -s SLACK_BOT_TOKEN +printf '\n' >&2 +printf 'New Slack app token: ' >&2 +IFS= read -r -s SLACK_APP_TOKEN +printf '\n' >&2 +export SLACK_BOT_TOKEN SLACK_APP_TOKEN +NEMOCLAW_NON_INTERACTIVE=1 $$nemoclaw channels add slack +unset SLACK_BOT_TOKEN SLACK_APP_TOKEN +$$nemoclaw rebuild --yes +``` + +### Telegram + +```bash +printf 'New Telegram bot token: ' >&2 +IFS= read -r -s TELEGRAM_BOT_TOKEN +printf '\n' >&2 +export TELEGRAM_BOT_TOKEN +NEMOCLAW_NON_INTERACTIVE=1 $$nemoclaw channels add telegram +unset TELEGRAM_BOT_TOKEN +$$nemoclaw rebuild --yes +``` + +### Discord + +```bash +printf 'New Discord bot token: ' >&2 +IFS= read -r -s DISCORD_BOT_TOKEN +printf '\n' >&2 +export DISCORD_BOT_TOKEN +NEMOCLAW_NON_INTERACTIVE=1 $$nemoclaw channels add discord +unset DISCORD_BOT_TOKEN +$$nemoclaw rebuild --yes +``` + +Omit `NEMOCLAW_NON_INTERACTIVE=1` and the token variables if you want `channels add` to prompt for replacement values and offer the rebuild interactively. + +## Rotate a Web Search Key + +Web search provider configuration and credential attachment are baked into the sandbox image. +Select the provider again and recreate the sandbox so the replacement becomes active. + + + +OpenClaw supports Brave or Tavily through NemoClaw onboarding. + +```bash +printf 'New Brave API key: ' >&2 +IFS= read -r -s BRAVE_API_KEY +printf '\n' >&2 +export BRAVE_API_KEY +NEMOCLAW_WEB_SEARCH_PROVIDER=brave \ + $$nemoclaw onboard --fresh --name --recreate-sandbox \ + --non-interactive --yes --yes-i-accept-third-party-software +unset BRAVE_API_KEY +``` + +To rotate a Tavily key instead, read and export `TAVILY_API_KEY` silently and select `NEMOCLAW_WEB_SEARCH_PROVIDER=tavily`. + + + + +Hermes uses Tavily for NemoClaw-managed web search. +Brave is not a supported Hermes web search backend. + +```bash +printf 'New Tavily API key: ' >&2 +IFS= read -r -s TAVILY_API_KEY +printf '\n' >&2 +export TAVILY_API_KEY +NEMOCLAW_WEB_SEARCH_PROVIDER=tavily \ + $$nemoclaw onboard --fresh --name --recreate-sandbox \ + --non-interactive --yes --yes-i-accept-third-party-software +unset TAVILY_API_KEY +``` + + + +## Remove and Re-register a Provider Credential + +Use `credentials reset` only when you need to remove an inference or web search provider before its replacement is available. +Run `$$nemoclaw credentials list` first, then pass the exact provider name from that output. + +```bash +$$nemoclaw credentials reset nvidia-prod --yes +``` + +Removing a provider interrupts requests that depend on it. +Re-register an inference provider with the inference rotation command above. +For web search, repeat the matching web search onboarding flow and recreate the sandbox. + +Do not pass a messaging bridge name to `credentials reset`. +Use the messaging rotation flow above to replace a token, or `$$nemoclaw channels remove ` to retire the integration. + +## Emergency Rotation After Key Compromise + +If a credential was exposed, act in this order: + +1. Revoke the exposed credential at the upstream provider before relying on any local cleanup. +2. For inference or web search, remove the exact provider shown by `$$nemoclaw credentials list` if a replacement is not immediately available. +3. For messaging, use `channels remove` to retire the integration or re-add the channel with a replacement token and rebuild. +4. Issue a replacement credential at the upstream provider. +5. Follow the matching rotation procedure on this page. +6. Complete a real request through the affected inference, search, or messaging integration. +7. Review recent sandbox logs for unexpected authentication failures or use of the retired credential: + + ```bash + $$nemoclaw logs --since 24h | grep -i "auth\|401\|403\|forbidden" + ``` + +## Rotate an Inference Key in CI/CD + +Supply credentials through the CI system's secret store and inject them as environment variables. +Non-interactive onboarding must include the third-party software acceptance flag. + +```yaml +- name: Rotate inference key + env: + NVIDIA_INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_API_KEY }} + run: >- + $$nemoclaw onboard --name + --non-interactive --yes --yes-i-accept-third-party-software +``` + +Do not commit credential values or host-side NemoClaw state to the repository. +Plan for rebuild downtime when automating messaging rotation and recreation downtime when automating web search rotation. + +## Verify the Replacement + +`$$nemoclaw credentials list` confirms that a provider exists, but it does not reveal or validate the stored value. +`$$nemoclaw status` performs reachability checks without sending cloud API keys, so even an HTTP `401` or `403` can count as reachable. +`$$nemoclaw inference get` reports the active route and does not authenticate a model request. + +Complete a real request through the rotated integration before declaring the rotation successful. + + + +Verify an inference key by running an isolated OpenClaw turn and confirming that it returns the requested content. + +```bash +$$nemoclaw agent --session-id credential-check \ + -m "Reply with only: credential-check-ok" +``` + + + + +Verify an inference key by forwarding the Hermes API and making a chat-completions request with the onboarded model. + +```bash +TOKEN=$($$nemoclaw gateway-token --quiet) +openshell forward start --background 8642 +curl -sN http://127.0.0.1:8642/v1/chat/completions \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"model":"","messages":[{"role":"user","content":"Reply with credential-check-ok"}],"stream":false}' +``` + + + +To verify messaging, send a test message from an allowed account and confirm the sandbox receives it and responds. +To verify web search, ask the agent to perform a search and confirm that the tool returns current results without an authentication error. + +## Related Pages + +- [Credential Storage](credential-storage): how NemoClaw stores and protects credentials. +- [Runtime Controls](../manage-sandboxes/runtime-controls): which changes take effect at runtime and which require recreation. +- [Security Best Practices](best-practices): recommended key scopes and operational hygiene. +- [Switch Inference Providers](../inference/switch-inference-providers): change the inference provider rather than rotate its key. diff --git a/test/credential-rotation-docs.test.ts b/test/credential-rotation-docs.test.ts new file mode 100644 index 00000000000..da3d1fef17e --- /dev/null +++ b/test/credential-rotation-docs.test.ts @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { readFileSync } from "node:fs"; +import path from "node:path"; + +import { describe, expect, it } from "vitest"; + +const DOC_PATH = "docs/security/credential-rotation.mdx"; + +function readGuide(): string { + return readFileSync(path.join(process.cwd(), DOC_PATH), "utf8"); +} + +function fencedBlocks(text: string, language: string): string[] { + const pattern = new RegExp("```" + language + "\\n([\\s\\S]*?)```", "g"); + return [...text.matchAll(pattern)].map((match) => match[1] ?? ""); +} + +describe("credential rotation documentation", () => { + it("keeps every non-interactive onboard example executable", () => { + const examples = [ + ...fencedBlocks(readGuide(), "bash"), + ...fencedBlocks(readGuide(), "yaml"), + ].filter((block) => block.includes("onboard") && block.includes("--non-interactive")); + + expect(examples.length).toBeGreaterThan(0); + for (const example of examples) { + expect(example).toContain("--name "); + expect(example).toContain("--yes-i-accept-third-party-software"); + } + }); + + it("uses normal onboarding instead of interrupted-session resume", () => { + expect(readGuide()).not.toContain("--resume"); + }); + + it("keeps replacement credentials out of command text (#6266)", () => { + const guide = readGuide(); + const credentialVariables = [ + "NVIDIA_INFERENCE_API_KEY", + "SLACK_BOT_TOKEN", + "SLACK_APP_TOKEN", + "TELEGRAM_BOT_TOKEN", + "DISCORD_BOT_TOKEN", + "BRAVE_API_KEY", + "TAVILY_API_KEY", + ]; + + for (const variable of credentialVariables) { + expect(guide).toMatch(new RegExp(`IFS= read -r -s ${variable}`)); + expect(guide).toMatch(new RegExp(`unset [^\\n]*\\b${variable}\\b`)); + expect(guide).not.toMatch(new RegExp(`${variable}=[^\\s$]`)); + } + }); + + it("documents messaging rebuilds and web search recreation", () => { + const guide = readGuide(); + const bash = fencedBlocks(guide, "bash"); + + for (const channel of ["slack", "telegram", "discord"]) { + const example = bash.find((block) => block.includes(`channels add ${channel}`)); + expect(example, channel).toBeDefined(); + expect(example, channel).toContain("rebuild --yes"); + } + + const searchExamples = bash.filter((block) => block.includes("NEMOCLAW_WEB_SEARCH_PROVIDER")); + expect(searchExamples.length).toBeGreaterThan(0); + for (const example of searchExamples) { + expect(example).toContain("--fresh"); + expect(example).toContain("--recreate-sandbox"); + } + }); + + it("uses real provider names and separates configuration checks from live proof", () => { + const guide = readGuide(); + + expect(guide).toContain("credentials reset nvidia-prod --yes"); + expect(guide).toContain("Per-sandbox messaging bridge names are not resettable credentials"); + expect(guide).toContain("Complete a real request through the rotated integration"); + expect(guide).not.toContain("alpha-nvidia-inference"); + expect(guide).not.toContain("alpha-slack"); + expect(guide).not.toContain("PROVIDER_KEY=new-value"); + }); + + it("authenticates the Hermes verification request", () => { + const example = fencedBlocks(readGuide(), "bash").find((block) => + block.includes("/v1/chat/completions"), + ); + + expect(example).toBeDefined(); + expect(example).toContain("gateway-token --quiet"); + expect(example).toContain("Authorization: Bearer $TOKEN"); + }); +});