Skip to content

fix(secret): store macos secrets in keychain via stdin - #436

Merged
mogul merged 3 commits into
mainfrom
verify/acq-secret-encryption
Sep 1, 2026
Merged

mogul merged 3 commits into
mainfrom
verify/acq-secret-encryption

Conversation

@mogul

@mogul mogul commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Context

acq secret was falling back to host plaintext 0600 files on macOS to avoid putting secret values on argv with security add-generic-password -w VALUE. A host-side probe confirmed that /usr/bin/security -i can 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

  • Add a macOS Keychain write path using /usr/bin/security -i with stdin command-stream input.
  • Keep secret values off argv and avoid security PATH hijacking by using /usr/bin/security in production.
  • Preserve compatibility with legacy macOS file-backed secrets by reading them as a fallback.
  • Remove same-key legacy file fallback entries after successful Keychain re-save.
  • Use the non-secret key index so acq secret ls can list macOS Keychain-backed entries.
  • Add a host-side macOS probe script and stubbed offline tests for the Keychain path.
  • Update docs and ADR-0011 to describe the new macOS behavior.

Verification

  • bash -n acq.backends/secret-store.sh scripts/probe-macos-keychain-stdin test/bats/71b-msb-secret-store-backends.bats
    • PASS
  • git 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
    • PASS
  • ./scripts/test-acq-bats test/bats/71b-msb-secret-store-backends.bats
    • PASS, 9/9
  • ./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
    • PASS, 45/45
  • ./scripts/test-acq-bats
    • PASS, 404/404
  • ./scripts/probe-macos-keychain-stdin
    • BLOCKED in this Linux sandbox as expected: fails closed with "macOS is required"
    • Live host probe was run manually on macOS 26.6.2 and passed: basic, special-character, and overwrite security -i cases succeeded; bare -w prompt form was skipped by default.

Rollback

Revert this PR. macOS will return to the previous argv-safe 0600 file fallback behavior for new writes. Existing Keychain entries can be removed with acq secret rm or left unused if the old file fallback path is restored.

Security Impact

  • Improves macOS secret storage by using the OS Keychain instead of plaintext file fallback when /usr/bin/security is available.
  • Keeps secret values off argv by using security -i over stdin.
  • Avoids command hijacking by invoking /usr/bin/security directly in production.
  • Rejects unsafe service/sandbox names before constructing a Keychain command stream.
  • Removes same-key legacy plaintext fallback files after successful Keychain re-save.

@mogul
mogul requested a review from a team as a code owner September 1, 2026 05:47

@wz-gsa wz-gsa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mogul
mogul enabled auto-merge (squash) September 1, 2026 19:06
@mogul
mogul merged commit 20ab8c7 into main Sep 1, 2026
5 checks passed
@mogul
mogul deleted the verify/acq-secret-encryption branch September 1, 2026 19:52
mogul pushed a commit that referenced this pull request Oct 21, 2026
## 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants