Repository navigation
fix(secret): store macos secrets in keychain via stdin - #436
Conversation
wz-gsa
left a comment
There was a problem hiding this comment.
Adversarial security + QA review, both with live macOS reproduction against the real /usr/bin/security binary (not simulated), plus an independent re-run of the full test suite.
The core security claim is genuine and verified, not just asserted. Reproduced live: values never appear in argv/process table; security -i does NOT perform shell-style $VAR/backtick expansion inside double-quoted tokens on this macOS build (tested with a literal $VAR and backtick-bearing value — round-tripped byte-for-byte, no expansion); a failed keychain write correctly returns 1 and never falls through to a silent plaintext write. ACQ_SECRET_ALLOW_ARGV is fully removed with zero stale references anywhere in the tree. The stricter [A-Za-z0-9_-] service/sandbox validation causes no regression against any real service name used anywhere in the codebase or docs.
One real, non-blocking security finding. _acq_secret_security_i_quote() escapes only \ and " — a raw embedded newline in the secret value is not escaped, and I confirmed live this WOULD let a value inject a second security -i command (e.g. a rogue add-generic-password) if such a value ever reached the writer. Every actual code path is safe today because the value is first read via a single-line IFS= read -r (structurally incapable of holding an embedded \n) — confirmed for the TTY-masked read, the piped-stdin read, and the acq_secret_import path (which additionally has an explicit newline/TAB rejection, verified enforced in code, not just asserted in the docstring). The one exception: the test-only ACQ_SECRET_TEST_VALUE escape hatch has no explicit gate — currently unreachable in production (no production path sets it), but if a future change ever wires a production secret through that same branch, the newline would silently truncate rather than being rejected. Recommend a follow-up issue applying the same explicit-rejection pattern _acq_import_value_is_storable already uses.
Test-result claim correction: the PR body states 404/404 for the full suite. I independently reproduced 401/404 (confirmed identically by both reviewers) — the 3 failures are in files byte-identical to main (30-dispatch-routing.bats, 70-msb-backend.bats, 90-sbx-startup-kit.bats) and reproduce the same way against main itself, so they're pre-existing host-environment flakes (real-git-identity env leakage in my case), not a regression from this diff. The targeted file (71b-msb-secret-store-backends.bats) is a clean 9/9, matching the PR body.
One genuine, previously-undocumented limitation found: security find-generic-password -w hex-mangles the read-back of any value containing control characters or non-ASCII bytes (verified: the write is byte-correct via -g's raw hex dump; only the -w read formatting is affected). Low-probability given acq secrets are API tokens (overwhelmingly ASCII), but neither the bats stub, the probe script, nor the docs mention it — worth a doc note or tracked follow-up.
Everything else verified clean: probe script and production quoting functions are byte-identical (diffed directly — the "verified live on macOS 26.6.2" claim genuinely validates the shipped code, not a divergent scheme); ADR-0011/BACKEND_GUIDE.md's legacy-file-fallback description matches the live code path exactly; no other doc anywhere in the tree describes the old ALLOW_ARGV-gated behavior.
Two non-blocking follow-ups recommended (the ACQ_SECRET_TEST_VALUE gate, the non-ASCII read-side caveat); neither blocks this PR, which is a well-engineered, correctly fail-closed improvement over the previous argv-exposed/silent-file-fallback behavior. Approved.
## Summary Moves `PATTERNS_KIT_REF` from v1.12.0 (`56d1f5f`) to agentic-coding-patterns v1.15.0 (`f3c6acb`), so the built-in kit bundle picks up: - **opencode kit → OpenCode 2.0.30** (GSA-TTS/agentic-coding-patterns#499). OpenCode 2.0.0–2.0.29 ignore config `instructions` (anomalyco/opencode#51341, fixed in 2.0.30), so a sandbox on an image without a baked OpenCode (`devenv-shell`, or the `devenv-shell` default proposed in #584) currently runs the bundled 2.0.22 and silently drops `instructions`-based conventions. - **paseo kit → Paseo 0.12.0** (GSA-TTS/agentic-coding-patterns#504): fixes for OpenCode v2 under Paseo (daemon OOM on long v2 streams, multi-select answers, HTTP 500s instead of tool errors) and the host-sleep daemon-status fix. - **Kit verify scripts gain a "could not check" verdict** (GSA-TTS/agentic-coding-patterns#460); it only affects kit verify output. Also v1.13.0: usai-provider's host-side catalog normalizer (#436), the paseo identity-gate script (#453), and the team-kit and personal-kit templates under `acq-kits/examples/`. None changes what acq applies by default beyond the two kits above. Also v1.15.0: refreshed usai-provider model catalog (GSA-TTS/agentic-coding-patterns#519, GSA-TTS/agentic-coding-patterns#484), the OpenDesign opt-in kit (GSA-TTS/agentic-coding-patterns#438), and the provider-neutral input contract (GSA-TTS/agentic-coding-patterns#518). acq's provider-facts loader reads `provider-facts/usai.env`, which v1.15.0 does not change. The doc examples and Bats assertions that spell the pin move with it. KNOWN_FAILURE_MODES §9 now names 2.0.30 as the version the kit installs and notes its 15-minute default `chunkTimeout` (the timeout message and retry limits are unchanged from 2.0.22). ## Verification On the v1.15.0 pin: - Focused Bats: `135-configure`, `95-kit-ref-persistence`, `36-agent-catalog`, `50-kit-list-completeness` (63/63) - markdownlint on the changed docs On the earlier v1.14.0 pin (not re-run for v1.15.0): - `npm run lint:md` - `RUN_ACQ=1 scripts/verify-backends --only msb` on macOS, msb 0.7.8: 16 passed, 1 skipped (playbook fetch, no `gh` login), run with `TMPDIR` set to its resolved `/private/var/...` path to get past an unrelated create failure that also hits `main` (#594) - Downstream, with this pin on msb 0.7.8 and `ACQ_IMAGE=devenv-shell:1.0.0`: a team kit's end-to-end verify passed 71/71 (OpenCode 2.0.30 runs and loads `instructions`); with the paseo kit added, Paseo 0.12.0 drove an OpenCode 2.0.30 agent that received the team `instructions`, and its sessions survived a backup, `acq rm`, recreate and restore ## Rollback Revert this PR to restore the v1.12.0 bundle.
Context
acq secretwas falling back to host plaintext0600files on macOS to avoid putting secret values on argv withsecurity add-generic-password -w VALUE. A host-side probe confirmed that/usr/bin/security -ican write generic-password items from stdin when the command stream is terminated by EOF.This PR also includes
679f4472856b08b507ba2d92236a7cc47bb5bae5(fix(test): derive expected install.sh version instead of hardcoding it) so the full offline suite reflects the current release tag.AI-assisted: yes.
Plan
/usr/bin/security -iwith stdin command-stream input.securityPATH hijacking by using/usr/bin/securityin production.acq secret lscan list macOS Keychain-backed entries.Verification
bash -n acq.backends/secret-store.sh scripts/probe-macos-keychain-stdin test/bats/71b-msb-secret-store-backends.batsgit diff --check -- acq.backends/secret-store.sh scripts/probe-macos-keychain-stdin test/bats/71b-msb-secret-store-backends.bats docs/BACKEND_GUIDE.md docs/adr/0011-msb-backend-and-neutral-kits.md./scripts/test-acq-bats test/bats/71b-msb-secret-store-backends.bats./scripts/test-acq-bats test/bats/40-secret-set.bats test/bats/45-secret-store-unit.bats test/bats/71b-msb-secret-store-backends.bats./scripts/test-acq-bats./scripts/probe-macos-keychain-stdinsecurity -icases succeeded; bare-wprompt form was skipped by default.Rollback
Revert this PR. macOS will return to the previous argv-safe
0600file fallback behavior for new writes. Existing Keychain entries can be removed withacq secret rmor left unused if the old file fallback path is restored.Security Impact
/usr/bin/securityis available.security -iover stdin./usr/bin/securitydirectly in production.