security: redact resolved API keys from models.json#28645
security: redact resolved API keys from models.json#28645dsantoreis wants to merge 8 commits intoopenclaw:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd2e5fde98
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (typeof apiKey === "string" && apiKey.trim() && !ENV_VAR_NAME_RE.test(apiKey.trim())) { | ||
| // This apiKey is a resolved secret (not an env var name) — omit it. | ||
| const { apiKey: _stripped, ...rest } = entry as Record<string, unknown>; | ||
| redacted[key] = rest as ProviderConfig; |
There was a problem hiding this comment.
Keep a placeholder apiKey instead of removing it
Removing apiKey outright for any non-env-var value can break profile-authenticated providers during model discovery. resolveImplicitProviders() and normalizeProviders() intentionally populate apiKey from auth profiles because providers that define models need that field for pi model registration (src/agents/models-config.providers.ts), but this redaction deletes those profile-derived values before models.json is consumed by ModelRegistry in resolveModel() (src/agents/pi-embedded-runner/model.ts). In environments using auth profiles (and no provider env var), this can cause provider models to disappear and surface as Unknown model errors.
Useful? React with 👍 / 👎.
Greptile SummaryAddresses a security vulnerability where resolved API keys were persisted in plaintext to Key changes:
Security impact:
How it works: Confidence Score: 4/5
Last reviewed commit: bd2e5fd |
| function redactApiKeysForPersistence( | ||
| providers: ModelsConfig["providers"], | ||
| ): ModelsConfig["providers"] { | ||
| if (!providers) { | ||
| return providers; | ||
| } | ||
| const redacted: Record<string, ProviderConfig> = {}; | ||
| for (const [key, entry] of Object.entries(providers)) { | ||
| if (!entry) { | ||
| redacted[key] = entry; | ||
| continue; | ||
| } | ||
| const apiKey = (entry as Record<string, unknown>).apiKey; | ||
| if (typeof apiKey === "string" && apiKey.trim() && !ENV_VAR_NAME_RE.test(apiKey.trim())) { | ||
| // This apiKey is a resolved secret (not an env var name) — omit it. | ||
| const { apiKey: _stripped, ...rest } = entry as Record<string, unknown>; | ||
| redacted[key] = rest as ProviderConfig; | ||
| } else { | ||
| redacted[key] = entry; | ||
| } | ||
| } | ||
| return redacted; | ||
| } |
There was a problem hiding this comment.
Consider adding unit tests for this security-critical function to verify:
- Env var names preserved (e.g.,
OPENAI_API_KEY) - Resolved secrets stripped (e.g.,
sk-proj-...,hf_...) - Edge cases handled (empty strings, whitespace, single char)
- Other provider fields preserved (
baseUrl,models, etc.)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/models-config.ts
Line: 116-138
Comment:
Consider adding unit tests for this security-critical function to verify:
- Env var names preserved (e.g., `OPENAI_API_KEY`)
- Resolved secrets stripped (e.g., `sk-proj-...`, `hf_...`)
- Edge cases handled (empty strings, whitespace, single char)
- Other provider fields preserved (`baseUrl`, `models`, etc.)
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c1a56b609
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * (e.g. "OPENAI_API_KEY"). These are safe to persist in models.json because | ||
| * they are *references*, not the secret value itself. | ||
| */ | ||
| const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9_]{1,127}$/; |
There was a problem hiding this comment.
Redact uppercase API secrets before persisting models.json
The redaction guard treats any all-caps token matching ^[A-Z][A-Z0-9_]{1,127}$ as an env-var reference, so literal API keys in that shape are written to disk unchanged. This means users with uppercase-only credentials (for example custom proxy tokens) still leak plaintext secrets into models.json, which defeats the security fix for that input class.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5cac67fac6
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| continue; | ||
| } | ||
| const apiKey = (entry as Record<string, unknown>).apiKey; | ||
| if (typeof apiKey === "string" && apiKey.trim() && !ENV_VAR_NAME_RE.test(apiKey.trim())) { |
There was a problem hiding this comment.
Redact uppercase API key literals before persisting
redactApiKeysForPersistence() currently treats any all-caps token matching ^[A-Z][A-Z0-9_]{1,127}$ as an environment-variable reference and keeps it in models.json. If a resolved/profile API key itself is uppercase (for example proxy or enterprise tokens), it will be written to disk in plaintext, so the at-rest secret redaction introduced in this commit is bypassed for that credential shape.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75cfeeaf59
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * all-uppercase API keys that happen to pass the regex (e.g. custom proxy tokens). | ||
| */ | ||
| function isEnvVarReference(value: string): boolean { | ||
| return ENV_VAR_NAME_RE.test(value) && value in process.env; |
There was a problem hiding this comment.
Preserve env-var API key refs even when unset
isEnvVarReference() now requires the token to exist in process.env, so a valid configured reference like apiKey: "OPENAI_API_KEY" is treated as a secret and rewritten to REDACTED whenever ensureOpenClawModelsJson() runs in an environment where that var is not currently exported (common for different launch contexts). Because merge mode preserves any non-empty existing apiKey, that redacted value then keeps winning on later writes, so the original reference cannot recover even after the env var is restored; this can break workflows that rely on persisted env-var references rather than profile keys.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a1d7a48d4
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| continue; | ||
| } | ||
| const apiKey = (entry as Record<string, unknown>).apiKey; | ||
| if (typeof apiKey === "string" && apiKey.trim() && !ENV_VAR_NAME_RE.test(apiKey.trim())) { |
There was a problem hiding this comment.
Redact uppercase API key literals before writing models.json
Treating any token that matches ^[A-Z][A-Z0-9_]{1,127}$ as an env-var reference allows real API keys with uppercase/underscore formats to be persisted in plaintext. In configurations where a resolved/profile key happens to use that shape (for example enterprise proxy tokens), redactApiKeysForPersistence() skips replacement and writes the secret to models.json, which bypasses the at-rest redaction this change is intended to enforce.
Useful? React with 👍 / 👎.
This comment was marked as spam.
This comment was marked as spam.
SecretRef and environment variable values were resolved to plaintext during provider normalization and then persisted to ~/.openclaw/agents/*/agent/models.json, defeating the purpose of at-rest secret protection. This commit strips resolved API keys before writing models.json. Environment variable *names* (e.g. OPENAI_API_KEY) are preserved as safe references, but actual key values (sk-..., hf_..., etc.) are omitted. The runtime auth chain (profiles → env → config → SecretRef) re-resolves keys on demand, so models.json does not need plaintext secrets. Existing models.json files with leaked keys are overwritten on next gateway start (mode=replace) or config change (mode=merge) — the merge path no longer copies plaintext apiKey values from the existing file. Fixes #28359
Addresses Codex review feedback: ModelRegistry requires the apiKey field to be present for provider registration. Deleting it entirely could cause 'Unknown model' errors for providers discovered via auth profiles. Now replaces resolved secrets with "REDACTED" placeholder instead of omitting the field.
Verifies that: - Resolved secret values (sk-...) are replaced with REDACTED placeholder - Environment variable names (UPPERCASE_PATTERN) are preserved as safe references Addresses Greptile review feedback requesting tests for the security-critical redaction function.
Fixes CI tsgo type check failure — ModelDefinitionConfig requires reasoning, input, cost, contextWindow, and maxTokens fields.
…erence Addresses Codex review: all-uppercase API keys (e.g. custom proxy tokens) could pass the ENV_VAR_NAME_RE regex and leak to disk. Now checks both the naming pattern AND existence in process.env. Also adds a third test case for uppercase tokens not in env, and fixes tsgo type compatibility for ModelDefinitionConfig in tests.
The process.env check broke existing tests where CONFIG_KEY (a literal apiKey from config, not an env var) was being redacted because it wasn't in the CI environment. Regex-only detection preserves any UPPER_CASE value as a safe reference, which matches the existing test expectations. Fixes CI: checks-windows and checks (bun) failures.
- Verify other provider fields (baseUrl, models, api) are preserved - Handle empty string apiKey without crashing - Test various secret key formats (sk-proj-*)
Summary
Fixes #28359 — SecretRef and environment variable values were resolved to plaintext during provider normalization and persisted to
~/.openclaw/agents/*/agent/models.json, defeating at-rest secret protection.Problem
After running
openclaw secrets configure && openclaw secrets apply, the gateway resolves SecretRef entries to their plaintext values and writes them directly tomodels.json:{ "providers": { "openai": { "apiKey": "sk-proj-abc123..." } } }This means every
models.jsonon disk contains plaintext API keys — a security risk, especially on multi-user systems or when backups include the~/.openclaw/agents/directory.Fix
Added
redactApiKeysForPersistence()that strips resolved API keys before writingmodels.json:OPENAI_API_KEY) are preserved — they are safe references, not secretssk-proj-...,hf_...) are omitted entirelymodels.jsondoesn't need plaintext secretsmodels.jsonfiles are overwritten on next gateway startDetection
Uses
/^[A-Z][A-Z0-9_]{1,127}$/to distinguish env var names from resolved secrets. This is the same pattern used elsewhere in the codebase (ENV_SECRET_TEMPLATE_RE).Files Changed (1 file, +44/-1 lines)
src/agents/models-config.tsredactApiKeysForPersistence(), apply before JSON.stringifyValidation
pnpm build✅tsc --noEmit✅ (zero type errors)oxfmt✅Risk Assessment