Skip to content

Comments

feat: add HashiCorp Vault credential backend#9

Merged
dedene merged 2 commits intomainfrom
feat/vault-backend
Feb 21, 2026
Merged

feat: add HashiCorp Vault credential backend#9
dedene merged 2 commits intomainfrom
feat/vault-backend

Conversation

@dedene
Copy link
Owner

@dedene dedene commented Feb 21, 2026

Summary

  • Add HashiCorp Vault as a 7th secret backend, spawning vault kv get -format=json via CLI (zero new Go deps)
  • Auto-detect KV-v1 vs KV-v2 responses (checks for both data + metadata fields)
  • Support jq pipe extraction: vault:secret/app/creds | .password
  • 6 new config fields: vault_binary, vault_addr, vault_skip_verify, vault_cacert, vault_namespace, vault_token_file
  • Hot-reload all vault settings on config file change
  • Wire through daemon, executor, and HTTP proxy
  • Add secret backends table to README

Design

Follows the existing CLI-spawning pattern (like op, bw, pass). User manages vault login externally — claw-wrap never authenticates itself. Tokens are read from ~/.vault-token or VAULT_TOKEN_FILE.

Test plan

  • Unit tests: parser, fetcher (KV-v1/v2/jq/errors), env builder, config getters, cache, dispatcher
  • go test ./... — all green
  • make dev (fmt + vet + build) — clean
  • Live E2E: claw-wrap daemon + claw-wrap check against a real Vault instance (KV-v1)

Spawn `vault kv get -format=json` via CLI, matching existing backend
pattern (op, bw, pass). Zero new Go dependencies.

- KV-v2 and KV-v1 auto-detection (checks for both data + metadata)
- jq pipe support: `vault:secret/app/creds | .password`
- Config fields: vault_binary, vault_addr, vault_skip_verify,
  vault_cacert, vault_namespace, vault_token_file
- Hot-reload of all vault settings via fsnotify
- Wired through daemon, executor, and HTTP proxy
- Secret backends table added to README
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds HashiCorp Vault as an additional credential backend, fetched via the vault CLI, and wires the new backend through daemon execution, HTTP proxy injection, config surface area, and documentation.

Changes:

  • Introduce vault: credential source parsing + Vault CLI fetcher with KV-v1/KV-v2 auto-detection and optional jq extraction.
  • Add Vault-related config fields/getters and propagate them through daemon/executor/proxy setup (including config hot-reload hooks).
  • Update docs/README and expand test coverage for the new backend.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/httpproxy/proxy.go Adds WithVaultBinary option and passes Vault binary override into credential fetch options for proxy injections.
internal/daemon/executor_env_test.go Formatting-only alignment change in executor env tests.
internal/daemon/executor.go Threads Vault binary override into credential fetching for tool env building and config file generation.
internal/daemon/daemon_test.go Removes trailing whitespace line.
internal/daemon/daemon.go Adds hot-reload wiring for Vault settings + passes Vault binary override into HTTP proxy construction and admin fetches.
internal/credentials/vault_test.go New unit tests for Vault fetcher (KV-v1/KV-v2), jq extraction, env builder, and error cases.
internal/credentials/vault.go New Vault backend implementation (CLI spawn, env overrides, KV response parsing).
internal/credentials/parser_test.go Adds parser tests for vault: sources and jq pipe syntax.
internal/credentials/parser.go Introduces BackendVault and parsing support for the vault: prefix.
internal/credentials/credentials_test.go Adds a unit test for the new WithVaultBinary fetch option.
internal/credentials/credentials.go Adds VaultBinary to fetch options and dispatches BackendVault to the Vault fetcher.
internal/credentials/cache.go Allows credential caching for Vault backend (alongside 1Password and Bitwarden).
internal/config/config_test.go Adds tests for Vault config getters (binary/addr/skip-verify/CA cert/namespace/token file).
internal/config/config.go Adds Vault config fields to ProxyConfig and implements Vault config getters.
docs/CONFIG.md Documents the vault: backend, config knobs, and cache scope update.
README.md Adds a “Secret Backends” table including Vault and jq extraction note.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ctx, cancel := context.WithTimeout(ctx, vaultCommandTimeout)
defer cancel()

cmd := exec.CommandContext(ctx, vaultBinary, "kv", "get", "-format=json", parsed.Path)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

parsed.Path is user-controlled config and is passed as the final CLI argument without a -- end-of-options marker. If the path begins with -, the Vault CLI may treat it as a flag (option injection) instead of a secret path. Add -- before parsed.Path (similar to fetchFromPass) to ensure the path is always interpreted as a positional argument.

Suggested change
cmd := exec.CommandContext(ctx, vaultBinary, "kv", "get", "-format=json", parsed.Path)
cmd := exec.CommandContext(ctx, vaultBinary, "kv", "get", "-format=json", "--", parsed.Path)

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 4e9362a — added -- before parsed.Path, matching the pass backend pattern at credentials.go:262.

Comment on lines 138 to 147
func vaultEnv() []string {
env := os.Environ()
addr, skipVerify, caCert, namespace, tokenFile := getVaultSettings()

if addr != "" {
env = append(env, "VAULT_ADDR="+addr)
}
if skipVerify {
env = append(env, "VAULT_SKIP_VERIFY=1")
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

vaultEnv() appends VAULT_* entries to os.Environ() without removing existing keys. Vault is a Go binary and os.LookupEnv returns the first occurrence, so if the daemon already has VAULT_ADDR/VAULT_TOKEN_FILE/etc set, these appended values will not override them (contrary to the intended config behavior). Build the env by filtering out any existing VAULT_* keys first (or use a map-based merge) before appending the configured overrides.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 4e9362avaultEnv() now strips the specific VAULT_* keys it's about to set before appending overrides. Unconfigured fields (empty string / nil) inherit ambient env.

Comment on lines 855 to 858
// GetVaultSkipVerify returns whether to skip Vault TLS verification.
func (c *Config) GetVaultSkipVerify() bool {
return c.Proxy != nil && c.Proxy.VaultSkipVerify
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

vault_skip_verify is a plain bool, so config cannot distinguish “unset” vs “explicitly false”. If the daemon environment already contains VAULT_SKIP_VERIFY=1, setting vault_skip_verify: false in config won’t be able to force verification on (especially once vaultEnv() is fixed to override env vars). Consider making this a *bool (tri-state) and, when non-nil, explicitly set VAULT_SKIP_VERIFY to 0/1 in the child env.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed in 4e9362avault_skip_verify is now *bool. nil = inherit ambient env, true sets VAULT_SKIP_VERIFY=1, false sets VAULT_SKIP_VERIFY=0. Matches existing pattern (RequireAuth, UsePTY, etc.).

Comment on lines 497 to 503
proxy := httpproxy.New(httpCfg, cfg.Credentials,
httpproxy.WithPassBinary(cfg.GetPassBinary()),
httpproxy.WithOPBinary(cfg.GetOPBinary()),
httpproxy.WithBWBinary(cfg.GetBWBinary()),
httpproxy.WithVaultBinary(cfg.GetVaultBinary()),
httpproxy.WithAuthToken(d.proxyAuthToken),
httpproxy.WithRequireAuth(requireAuth),
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The HTTP proxy is constructed with WithVaultBinary(cfg.GetVaultBinary()), but Daemon.reloadConfig() only restarts the proxy on listen/auth/CA changes and Proxy.ReloadConfig() does not update credOpts. This means changes to proxy.vault_binary (and other *_binary settings) won’t take effect until the proxy is restarted for other reasons. Either include these binary paths in the “needsRestart” check or add a way to update credential fetch options during reload.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

This is a pre-existing limitation for all backends — op_binary, bw_binary, and pass_binary all have the same issue in Proxy.ReloadConfig(). Not addressing in this PR to keep scope contained, but noted for a follow-up.

…tate skip_verify

- Add `--` end-of-options guard before path argument (prevents option
  injection if path starts with `-`)
- Rewrite vaultEnv() to strip managed VAULT_* keys before appending
  configured overrides, so config values actually take precedence
- Change vault_skip_verify to *bool tri-state: nil inherits ambient env,
  explicit true/false overrides VAULT_SKIP_VERIFY
@dedene dedene merged commit 12c0f03 into main Feb 21, 2026
2 checks passed
@dedene dedene deleted the feat/vault-backend branch February 21, 2026 14:20
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