did-btcr2-js

ADR 077: CLI Secret Handling for Bitcoin RPC Credentials

Status: Accepted

Date: 2026-07-07

Branch / PR: feat/cli-io-config

References: ADR 052, ADR 072, ADR 074

Context

The Bitcoin Core RPC password is the one CLI-held secret with no protection and no indirection. It is declared as a plain string field on the profile schema (config.ts:75, rpcPass?: string), copied verbatim through profileToOverrides (config.ts:231, btcRpcPass: profile.btc?.rpcPass), and merged straight into the RPC client’s password field in resolveConnectionConfig (config.ts:306). At every hop it is plaintext.

The introspection commands make that plaintext visible. config get profiles.<name>.btc.rpcPass and config list render whatever value the key holds through formatResult (output.ts:11-16), which serializes the payload as-is. So the routine act of inspecting configuration prints the RPC password into terminal scrollback, screen shares, and CI job logs, none of which are places a credential should land.

This is out of step with the only other secret the CLI handles. The keystore passphrase is never accepted from a command-line flag (which would leak into process listings and shell history), and it already supports a file and an environment variable as sources, with a single trailing newline trimmed so the input is source-independent (keystore/passphrase.ts:24-49). The RPC password has none of that: no env var, no file reference, no redaction. The README compounds the gap by showing a bare cleartext password in a config set example (README.md:257).

The connection-config merge itself is otherwise sound after ADR 074, and the RPC password already flows correctly through the flag -> env -> profile -> per-network-default precedence. The problem here is narrow: the secret is stored and displayed in the clear, and there is no way to keep it out of config.json.

Decision

  1. Redact secret-looking keys in printed output by default. config get and config list mask the value of rpcPass and of any key whose leaf name matches a secret-name pattern (pass, password, secret, token), printing a fixed placeholder in place of the value. The redaction is display-only: the stored config.json is untouched, and the value still flows normally into the RPC client at connection time. Passing --show-secrets prints the real values for deliberate debugging.

  2. Add a file/env secret-ref for the RPC password, mirroring the keystore-passphrase pattern. The rpcPass value accepts two indirection forms in addition to a literal: env:<VARNAME> reads the secret from the named environment variable, and file:<path> reads it from a file. A BTCR2_BTC_RPC_PASS_FILE environment variable names a file to read when no other source applies, matching how BTCR2_KEYSTORE_PASSPHRASE and --passphrase-file supply the keystore secret. Resolution trims at most one trailing newline (\r?\n$), identical to keystore/passphrase.ts:28,31, so a file written by echo behaves the same as an inline value. With a secret-ref in place, the secret need not live in config.json at all.

  3. Document the plaintext-at-rest tradeoff. The README states plainly that an rpcPass written directly into config.json is stored in cleartext (the file is mode 0600 but not encrypted), and recommends either the env:/file: secret-ref or an RPC-URL-embedded credential for anything sensitive. The config set example is changed away from a bare cleartext password so the docs stop modeling the weakest option.

Consequences

Rejected alternatives