fix(security): redact resolved secrets from models.json#37568
fix(security): redact resolved secrets from models.json#37568Bartok9 wants to merge 1 commit intoopenclaw:mainfrom
Conversation
When the gateway resolves secrets at startup (env vars, SecretRef with Vault/exec/file providers), the resolved plaintext values were being written to agents/*/agent/models.json files on disk. This undermined the security benefits of centralized secret management. This change adds a redaction step before writing to models.json that: - Preserves env var names (UPPER_SNAKE_CASE patterns like OPENAI_API_KEY) - Preserves special markers (ollama-local, aws-sdk) - Strips resolved secret values that look like actual API keys The file permissions on models.json are already set to 0o600 (owner-only), but this change ensures secrets don't end up on disk at all. Fixes openclaw#37512
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f66d33d10
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (trimmed === "ollama-local") { | ||
| return true; |
There was a problem hiding this comment.
Preserve OAuth placeholder apiKeys during redaction
The new marker allowlist only preserves ollama-local/aws-sdk, so providers that intentionally use non-secret placeholder strings (for example minimax-oauth and qwen-oauth) get stripped as if they were resolved secrets. resolveImplicitProviders() sets those placeholders for minimax-portal and qwen-portal (src/agents/models-config.providers.ts:65, src/agents/models-config.providers.ts:135, src/agents/models-config.providers.ts:943, src/agents/models-config.providers.ts:979), and the same module notes that pi’s ModelRegistry requires apiKey when providers define models (src/agents/models-config.providers.ts:521-543), so this redaction can remove OAuth providers from models.json and make their models unresolvable.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR introduces a security fix that redacts resolved plaintext API keys from Key changes:
Critical issue found: The
Both values contain lowercase letters and hyphens, so they fail the UPPER_SNAKE_CASE regex check and are not listed as special cases. As a result, Confidence Score: 1/5
Last reviewed commit: 9f66d33 |
| function isEnvVarNameOrMarker(value: string): boolean { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| return false; | ||
| } | ||
|
|
||
| // Synthetic local provider markers (e.g., "ollama-local") | ||
| if (trimmed === "ollama-local") { | ||
| return true; | ||
| } | ||
|
|
||
| // AWS SDK auth marker | ||
| if (trimmed === "aws-sdk") { | ||
| return true; | ||
| } | ||
|
|
||
| // Environment variable name pattern: UPPER_SNAKE_CASE | ||
| // Must start with letter, contain only uppercase letters, digits, underscores | ||
| // Examples: OPENAI_API_KEY, ANTHROPIC_API_KEY, AWS_ACCESS_KEY_ID | ||
| return /^[A-Z][A-Z0-9_]*$/.test(trimmed); | ||
| } |
There was a problem hiding this comment.
Missing OAuth placeholder markers in allow-list
isEnvVarNameOrMarker() only recognises "ollama-local" and "aws-sdk" as special markers, but models-config.providers.ts defines two OAuth placeholder values that are assigned to apiKey fields at runtime:
MINIMAX_OAUTH_PLACEHOLDER— the string constant"minimax-oauth"(line 65) assigned asapiKeyfor theminimax-portalproviderQWEN_PORTAL_OAUTH_PLACEHOLDER— the string constant"qwen-oauth"(line 135) assigned asapiKeyfor theqwen-portalprovider
Both constants contain lowercase letters and a hyphen, so they do not match ^[A-Z][A-Z0-9_]*$ and are not listed as special markers. As a result, redactResolvedSecrets() will strip them from the written models.json.
At runtime the system reads apiKey back from models.json to determine the auth mode. If the field is absent, OAuth flows for MiniMax Portal and Qwen Portal will silently fail.
Both constant values should be added to isEnvVarNameOrMarker() alongside "ollama-local" and "aws-sdk". A more future-proof approach would be to accept an explicit allow-list of known marker strings passed in from the call site, so new providers don't require changes in two separate places.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/models-config.ts
Line: 27-47
Comment:
**Missing OAuth placeholder markers in allow-list**
`isEnvVarNameOrMarker()` only recognises `"ollama-local"` and `"aws-sdk"` as special markers, but `models-config.providers.ts` defines two OAuth placeholder values that are assigned to `apiKey` fields at runtime:
- `MINIMAX_OAUTH_PLACEHOLDER` — the string constant `"minimax-oauth"` (line 65) assigned as `apiKey` for the `minimax-portal` provider
- `QWEN_PORTAL_OAUTH_PLACEHOLDER` — the string constant `"qwen-oauth"` (line 135) assigned as `apiKey` for the `qwen-portal` provider
Both constants contain lowercase letters and a hyphen, so they do **not** match `^[A-Z][A-Z0-9_]*$` and are **not** listed as special markers. As a result, `redactResolvedSecrets()` will strip them from the written `models.json`.
At runtime the system reads `apiKey` back from `models.json` to determine the auth mode. If the field is absent, OAuth flows for MiniMax Portal and Qwen Portal will silently fail.
Both constant values should be added to `isEnvVarNameOrMarker()` alongside `"ollama-local"` and `"aws-sdk"`. A more future-proof approach would be to accept an explicit allow-list of known marker strings passed in from the call site, so new providers don't require changes in two separate places.
How can I resolve this? If you propose a fix, please make it concise.| it("preserves special marker values like ollama-local", async () => { | ||
| const config = { | ||
| models: { | ||
| providers: { | ||
| ollama: { | ||
| apiKey: "ollama-local", | ||
| baseUrl: "http://localhost:11434", | ||
| models: [{ id: "llama3" }], | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| await ensureOpenClawModelsJson(config, agentDir); | ||
|
|
||
| const modelsJson = await fs.readFile(path.join(agentDir, "models.json"), "utf8"); | ||
| const parsed = JSON.parse(modelsJson); | ||
|
|
||
| expect(parsed.providers.ollama?.apiKey).toBe("ollama-local"); | ||
| }); |
There was a problem hiding this comment.
Missing test coverage for OAuth placeholder markers
The test suite covers "ollama-local" but has no cases for the minimax-portal and qwen-portal OAuth placeholder apiKey values—the constants "minimax-oauth" and "qwen-oauth" from models-config.providers.ts (lines 65 and 135). Those values contain lowercase + hyphen characters that do not match the UPPER_SNAKE_CASE regex, so they fall into the same bucket as resolved secrets and will be stripped. A test mirroring the ollama-local structure would have caught this gap before the bug shipped.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/models-config.redact-secrets.test.ts
Line: 95-114
Comment:
**Missing test coverage for OAuth placeholder markers**
The test suite covers `"ollama-local"` but has no cases for the `minimax-portal` and `qwen-portal` OAuth placeholder `apiKey` values—the constants `"minimax-oauth"` and `"qwen-oauth"` from `models-config.providers.ts` (lines 65 and 135). Those values contain lowercase + hyphen characters that do not match the UPPER_SNAKE_CASE regex, so they fall into the same bucket as resolved secrets and will be stripped. A test mirroring the `ollama-local` structure would have caught this gap before the bug shipped.
How can I resolve this? If you propose a fix, please make it concise.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
|
Thanks for the work on this PR and for contributing to this fix path. Closing this as superseded by #38955 (merged), which integrated the relevant fixes from this thread and completed the broader hardening scope for the secrets/models persistence cluster. The merged work now covers source-snapshot persistence, marker/provenance auth handling, audit/apply expansion (including models.json/header residues), and write-mode hardening/tests/docs in one end-to-end fix. Appreciate the contribution here. If there’s a remaining unique change from this PR that should still land, please call it out in a follow-up issue and reference #38955. |
Summary
When the gateway resolves secrets at startup (env vars, SecretRef with Vault/exec/file providers), the resolved plaintext values were being written to
agents/*/agent/models.jsonfiles on disk. This undermined the security benefits of centralized secret management.Changes
isEnvVarNameOrMarker()helper to distinguish between env var names and resolved secretsredactResolvedSecrets()function that strips resolved secrets while preserving env var namesensureOpenClawModelsJson()What gets preserved
OPENAI_API_KEY,ANTHROPIC_API_KEY,AWS_ACCESS_KEY_ID, etc. (UPPER_SNAKE_CASE)ollama-local,aws-sdkWhat gets redacted
sk-1234...,xai-abc...)Testing
models-config.redact-secrets.test.tsFixes #37512