Skip to content

Phase G: multi-backend secrets (keychain + AWS Secrets Manager)#10

Merged
nixrajput merged 2 commits into
mainfrom
feat/phase-g-secrets
Jun 25, 2026
Merged

Phase G: multi-backend secrets (keychain + AWS Secrets Manager)#10
nixrajput merged 2 commits into
mainfrom
feat/phase-g-secrets

Conversation

@nixrajput

@nixrajput nixrajput commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

The final Phase G cycle: two new secret-ref backends so connection passwords can come from a credential store instead of config. Both implement the existing secrets.Backend seam (which has named these schemes since Phase A), so this is purely additive — no seam or resolver changes.

  • OS keychainkeychain://<account> (service siphon) or keychain://<service>/<account> (any stored credential), via go-keyring: macOS Keychain, Windows Credential Manager, Linux Secret Service. No config, no network.
  • AWS Secrets Managerawssm://<secret-id>, or awssm://<secret-id>#<json-key> to pull one field from a JSON secret (the common SM shape). Reuses the same AWS credential chain as the S3 storage backend. Off by default (building it loads AWS config); enable with secrets.awssm: true (+ optional awssm_region).

Resolution order is env → keychain → awssm → passthrough; passthrough stays last, so a literal password with no scheme (even one containing a colon) still resolves as-is.

Testing

  • make test — 20 packages pass; make lint — 0 issues; go vet -tags=integration ./... clean.
  • keychain tested via go-keyring's in-memory mock (short + long form, not-found → CodeUser, bad ref); awssm via a faked GetSecretValue client (whole secret, JSON #key extraction, not-found, missing field, non-JSON-with-key); plus a full-chain dispatch test (each scheme routes to its backend, literal falls through to passthrough). No real-cloud integration test — the faked client and keyring mock cover the logic.

Notes

  • New deps: github.com/zalando/go-keyring and aws-sdk-go-v2/service/secretsmanager (the SDK core was already a dep from the storage cycle).
  • Error mapping matches the env backend: missing/unknown = CodeUser, transport = CodeSystem.
  • Phase G (the v1.0 "ops" pillar) is complete with this cycle — cloud storage, retention, audit/2FA/telemetry/schedule/tunnel, and now multi-backend secrets all merged.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for resolving secret references from the OS keychain and AWS Secrets Manager.
    • Expanded secret reference formats, including support for JSON field selection in AWS Secrets Manager values.
    • Added configuration options to enable AWS secret resolution and select an AWS region.
  • Bug Fixes

    • Unknown or unmatched secret references now remain as literal values instead of failing resolution.
  • Documentation

    • Updated docs and changelog to reflect the new secret backends and supported reference formats.

- Add two secret-ref backends on the existing secrets.Backend seam (no
  seam changes — the package already named these schemes):
  - keychain:// (OS credential store) via go-keyring — macOS Keychain,
    Windows Credential Manager, or Linux Secret Service.
    keychain://<account> uses service "siphon"; the
    keychain://<service>/<account> form addresses any stored credential.
  - awssm:// (AWS Secrets Manager) via the AWS SDK, reusing the same
    credential chain as S3 storage. awssm://<id>#<json-key> selects one
    field of a JSON secret (the common Secrets Manager shape). The SDK
    call sits behind a tiny interface so it is faked in tests.
- Register backends in order env → keychain → awssm → passthrough;
  passthrough stays last so a literal value with no scheme resolves
  as-is. awssm is off by default (building it loads AWS config); enable
  via the new secrets.awssm config flag (+ optional awssm_region).
- Errors map to the errs taxonomy: a missing key / unknown field is a
  CodeUser error, transport is CodeSystem (like env).
- Test keychain via the keyring mock, awssm via a faked client (whole
  secret, JSON #key, not-found, missing field), and the full backend
  chain dispatch. Document in docs/OPS.md; README + CHANGELOG note the
  Phase G ops pillar is now complete.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nixrajput, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 46 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17c81703-327d-41c5-bac3-403939bd5718

📥 Commits

Reviewing files that changed from the base of the PR and between e2284e1 and f5e5dea.

📒 Files selected for processing (5)
  • README.md
  • internal/secrets/awssm.go
  • internal/secrets/awssm_test.go
  • internal/secrets/keychain.go
  • internal/secrets/keychain_test.go
📝 Walkthrough

Walkthrough

The PR adds keychain and AWS Secrets Manager secret-ref backends, wires them into resolver construction with config gating, and updates tests and docs to reflect the supported schemes and literal fallback behavior.

Changes

Multi-backend secrets

Layer / File(s) Summary
Secrets config
internal/config/config.go
Adds an optional secrets config block with AWS Secrets Manager enablement and region fields.
Keychain backend
internal/secrets/keychain.go, internal/secrets/keychain_test.go, go.mod
Implements keychain:// resolution through go-keyring, supports short and service-qualified references, and adds related dependency entries.
AWS Secrets Manager backend
internal/secrets/awssm.go, internal/secrets/awssm_test.go, go.mod
Implements awssm:// resolution with optional JSON field selection and adds the Secrets Manager SDK dependency.
Resolver wiring and docs
internal/cli/root.go, internal/secrets/resolver_test.go, README.md, docs/OPS.md, CHANGELOG.md
Builds a resolver chain with Env, Keychain, optional AWSSM, and Passthrough, covers the chain in integration tests, and updates the docs to list the supported schemes and fallback behavior.

Sequence Diagram(s)

sequenceDiagram
  participant buildDeps
  participant buildResolver
  participant secrets.Env
  participant secrets.Keychain
  participant secrets.NewAWSSM
  participant secrets.Passthrough

  buildDeps->>buildResolver: build resolver from cfg
  buildResolver->>secrets.Env: add env backend
  buildResolver->>secrets.Keychain: add keychain backend
  alt cfg.secrets.awssm
    buildResolver->>secrets.NewAWSSM: create AWSSM backend
    secrets.NewAWSSM-->>buildResolver: AWSSM backend
  end
  buildResolver->>secrets.Passthrough: append literal fallback
  buildResolver-->>buildDeps: *secrets.Resolver
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A bunny hopped through env: so bright,
then nibbled keychain:// at night.
awssm:// twinkled, clear and true,
and plain old text hopped through too.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding multi-backend secrets support via keychain and AWS Secrets Manager.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase-g-secrets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nixrajput nixrajput self-assigned this Jun 25, 2026
@nixrajput

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
internal/secrets/keychain_test.go (1)

33-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the unresolved-secret sentinel here too.

This only checks CodeUser, so a regression that stops returning errs.ErrSecretUnresolved would still pass even though callers can match that sentinel.

Suggested fix
 	var e *errs.Error
 	if !errors.As(err, &e) || e.Code != errs.CodeUser {
 		t.Fatalf("missing key err = %v, want CodeUser", err)
 	}
+	if !errors.Is(err, errs.ErrSecretUnresolved) {
+		t.Fatalf("missing key err = %v, want ErrSecretUnresolved", err)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/secrets/keychain_test.go` around lines 33 - 39, The Keychain test
only verifies that Resolve returns a user error, so it can miss regressions
where the unresolved-secret sentinel is no longer returned. Update
TestKeychain_NotFoundIsUserError to also assert that the error matches
errs.ErrSecretUnresolved in addition to checking errs.CodeUser. Use the existing
Resolve method and the err returned from keychain://missing to confirm both the
code and sentinel remain intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/secrets/awssm.go`:
- Around line 66-68: Reject empty `#` selectors in `ParseAWSSMRef`/the
`strings.IndexByte(rest, '#')` parsing path instead of falling back to the
whole-secret branch when `jsonKey` is empty. Update the selector validation so
`awssm://...#` is treated as an invalid reference and returns an error, and add
a regression test covering this exact case to prevent the whole-secret path from
being taken again.
- Around line 73-75: The secret resolution path is still contextless, and
awssm.go is calling Secrets Manager with a background context. Update
Backend.Resolve and Resolver.Resolve to accept a context.Context, then thread
that context through the call chain into the GetSecretValue call in awssm.go.
Also update the callers, including the command flow in gate.go, to pass the
existing command context so secret lookups can be canceled or time out properly.

In `@internal/secrets/keychain.go`:
- Around line 40-43: Reject empty service references in the keychain ref parser
before any keyring access. Update the parsing/validation in the keychain
handling flow around the service/account split so that keychain:///acct (where
service is empty) is treated as an invalid ref and returns a bad-ref error
instead of reaching keyring.Get with an empty service. Use the existing keychain
reference parsing logic and the account validation branch to add an explicit
service-empty check.

In `@README.md`:
- Line 45: The README wording overstates the safety of secret refs by implying
plaintext passwords never live in config, but the resolver still allows literal
passthrough for plaintext values. Update the copy around the named
profiles/secret refs description to limit the “safe to commit” claim to actual
secret references only, and avoid saying all passwords are resolved at runtime
or never present in config; adjust the affected text in the README section that
describes env, keychain, and awssm handling.

---

Nitpick comments:
In `@internal/secrets/keychain_test.go`:
- Around line 33-39: The Keychain test only verifies that Resolve returns a user
error, so it can miss regressions where the unresolved-secret sentinel is no
longer returned. Update TestKeychain_NotFoundIsUserError to also assert that the
error matches errs.ErrSecretUnresolved in addition to checking errs.CodeUser.
Use the existing Resolve method and the err returned from keychain://missing to
confirm both the code and sentinel remain intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3b44cde-da77-48eb-906e-12a008b34885

📥 Commits

Reviewing files that changed from the base of the PR and between c289bda and e2284e1.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • CHANGELOG.md
  • README.md
  • docs/OPS.md
  • go.mod
  • internal/cli/root.go
  • internal/config/config.go
  • internal/secrets/awssm.go
  • internal/secrets/awssm_test.go
  • internal/secrets/keychain.go
  • internal/secrets/keychain_test.go
  • internal/secrets/resolver_test.go

Comment thread internal/secrets/awssm.go
Comment thread internal/secrets/awssm.go Outdated
Comment thread internal/secrets/keychain.go
Comment thread README.md Outdated
- awssm: reject an empty "#" selector (awssm://id#) as malformed instead
  of silently falling back to the whole-secret path. Bound the
  GetSecretValue call with a 15s timeout so an unreachable Secrets
  Manager can't hang resolution — the Backend.Resolve seam is contextless
  (a Phase-A interface; threading context through it touches every
  backend + profile resolution + every verb), so the timeout is applied
  at the one call that does network I/O; full context-threading is left
  as a separate refactor.
- keychain: reject keychain:///acct (empty service) as a bad ref instead
  of calling keyring.Get("", "acct"), which would be backend-dependent.
- README: temper the secret-ref wording — passthrough means a literal
  password DOES live in config, so don't claim plaintext "never" lives
  there or that the file is unconditionally "safe to commit".
- Add regression tests for both new guards (empty # selector, empty
  keychain service).
@nixrajput nixrajput merged commit fe82615 into main Jun 25, 2026
5 checks passed
@nixrajput nixrajput deleted the feat/phase-g-secrets branch June 25, 2026 22:32
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.

1 participant