fix(config): harden RAFTER_API_KEY handling — 0600 perms, redaction, wire backend.apiKey (sable-q9to) - #173
Merged
Merged
Conversation
…ackend.apiKey (sable-q9to) Three credential-handling gaps (both Node + Python; intended env/flag path was already fine): 1. Plaintext, world-readable. ConfigManager.save() wrote ~/.rafter/config.json with the default umask (typically 0644). Now writes 0600 (dir 0700) and chmods an existing looser file on the next write — it can hold a backend key. 2. Unredacted echo. `config show`/`get`, the `config set` confirmation, and the MCP get_config tool + rafter://config / rafter://policy resources serialized the full config verbatim, handing any stored key to the terminal / MCP client. Added redactConfigSecrets / maskSecretValue (mask values under keys matching api_?key|token|secret|password|credential → "abcd****"); applied at every render path. Pure — the stored config is never mutated. 3. Dead credential path. backend.apiKey existed in the schema but resolveKey never read it. Wired it as the LOWEST-precedence source: --api-key flag > RAFTER_API_KEY env > global config backend.apiKey. Read only from the global ~/.rafter/config.json (load(), never loadWithPolicy/.rafter.yml — verified it does not merge backend.*), so a hostile repo cannot inject a key that redirects scans to another account. Secure-design pass recorded on the bead: it's a bearer token, so 0600 + display redaction is the right boundary (encrypting a local config just moves a KEK next to the ciphertext); precedence mirrors gh/aws/npm. Tests (both suites): redaction helpers, 0600 on fresh + tightened-on-rewrite, and resolveKey precedence flag>env>config. Node 67 / Python 19 config tests pass; broader config+mcp+api suites green. CLI_SPEC.md documents the resolution order, redaction, perms, and the global-only trust boundary. Note: Node key is backend.apiKey, Python is backend.api_key (dataclass snake_case) — same value, per-language path; redaction regex covers both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…le-q9to) The redaction work added redactConfigSecrets/isSecretConfigKey/maskSecretValue to config-manager, which server.ts now imports — but the vi.mock in mcp-server-integration.test.ts only stubbed ConfigManager, so those imports resolved to undefined and 4 get_config/resource tests threw. Spread the real module (keeping the pure redaction helpers real) and override only ConfigManager. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 20, 2026
Closed
Merged
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes sable-q9to. Three credential-handling gaps in
RAFTER_API_KEYhandling (both Node + Python). The intended env-var /--api-keypath was already solid (HTTPS,x-api-keyheader only, not logged); these are the storage/display/dead-path gaps surfaced during the secbolt credential spike.Gaps fixed
ConfigManager.save()wrote~/.rafter/config.jsonwith the default umask (typically0644). Now writes0600(dir0700) and chmods an existing looser-perm file on the next write.config show/get, theconfig setconfirmation, and the MCPget_configtool +rafter://config/rafter://policyresources serialized the full config verbatim — handing any stored key to the terminal or MCP client. AddedredactConfigSecrets/maskSecretValue(mask values under keys matchingapi_?key|token|secret|password|credential→abcd****), applied at every render path. Pure — the stored config is never mutated.backend.apiKeyexisted in the schema butresolveKeynever read it. Wired as the lowest-precedence source:--api-keyflag →RAFTER_API_KEYenv → global configbackend.apiKey.Security design
0600plaintext, not encryption: it's a bearer token (presented verbatim → can't hash); encrypting a local CLI config needs a KEK the CLI reads unattended, sitting right next to the ciphertext ("keys next to data"). So the boundary is file perms + display redaction — same asgh/aws/npm.~/.rafter/config.jsonviaload()—loadWithPolicy()never mergesbackend.*, so a hostile project-local.rafter.ymlcannot inject an API key that redirects scans to an attacker's account. Confirmed empirically (a planted.rafter.yml backend.apiKeydoes not appear inconfig showandresolveKeyrefuses it).rafter-secure-designbefore coding (decisions recorded on the bead); refuse-list clean (no homegrown crypto, opaque bearer token, revocation is backend-side).Tests
New
config-secret-handlingsuites in both languages: redaction helpers (mask/leaf-match/no-mutation),save()writes0600on fresh and tightens an existing0644file, andresolveKeyprecedenceflag > env > config. Node 67 / Python 19 config tests pass; broader config + mcp + api suites green. Secrets scan clean on all changed files.rafter run(remote SAST) not run — noRAFTER_API_KEYin this env; recommend CI.Notes
backend.apiKey; Python isbackend.api_key(dataclass snake_case) — same value, per-language path; the redaction regex (api_?key) covers both.CLI_SPEC.mddocuments the resolution order, redaction,0600, and the global-only trust boundary.🤖 Generated with Claude Code