From ce30aaf5a01db7f2b4ced113f75a9c5008a87315 Mon Sep 17 00:00:00 2001 From: "Dmitry K. Anisimov" Date: Fri, 29 Nov 2024 00:08:00 +0200 Subject: [PATCH] feat: Add keychain provider for reading text/yaml/json secrets Signed-off-by: Dmitry K. Anisimov --- README.md | 15 ++++++- go.mod | 1 + pkg/providers/keychain/keychain.go | 67 ++++++++++++++++++++++++++++++ vals.go | 6 +++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 pkg/providers/keychain/keychain.go diff --git a/README.md b/README.md index 5e239b4..52422d3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ It supports various backends including: - HCP Vault Secrets - Bitwarden - HTTP JSON +- Keychain - Use `vals eval -f refs.yaml` to replace all the `ref`s in the file to actual values and secrets. - Use `vals exec -f env.yaml -- ` to populate envvars and execute the command. @@ -216,6 +217,7 @@ Please see the [relevant unit test cases](https://github.com/helmfile/vals/blob/ - [Google GCS](#google-gcs) - [SOPS](#sops) powered by [sops](https://github.com/getsops/sops) - [Terraform (tfstate)](#terraform-tfstate) powered by [tfstate-lookup](https://github.com/fujiwara/tfstate-lookup) +- [Keychain](#keychain) - [Echo](#echo) - [File](#file) - [Azure Key Vault](#azure-key-vault) @@ -592,6 +594,18 @@ Examples: - `ref+sops://path/to/file#/foo/bar` reads `path/to/file` as a `yaml` file and returns the value at `foo.bar`. - `ref+sops://path/to/file?format=json#/foo/bar` reads `path/to/file` as a `json` file and returns the value at `foo.bar`. +### Keychain + +Keychain provider is going to be available on macOS only. It reads a secret from the macOS Keychain. + +- `ref+echo://KEY1/[#/path/to/the/value]` + +Examples: + +- `security add-generic-password -U -a ${USER} -s "secret-name" -D "vals-secret" -w '{"foo":{"bar":"baz"}}'` - will create a secret in the Keychain with the name `secret-name` and the value `{"foo":{"bar":"baz"}}`, `vals-secret` is required to be able to find the secret in the Keychain. +- `echo 'foo: ref+keychain://secret-name' | vals eval -f -` - will read the secret from the Keychain with the name `secret-name` and replace the `foo` with the secret value. +- `echo 'foo: ref+keychain://secret-name#/foo/bar' | vals eval -f -` - will read the secret from the Keychain with the name `secret-name` and replace the `foo` with the value at the path `$.foo.bar`. + ### Echo Echo provider echoes the string for testing purpose. Please read [the original proposal](https://github.com/roboll/helmfile/pull/920#issuecomment-548213738) to get why we might need this. @@ -603,7 +617,6 @@ Examples: - `ref+echo://foo/bar` generates `foo/bar` - `ref+echo://foo/bar/baz#/foo/bar` generates `baz`. This works by the host and the path part `foo/bar/baz` generating an object `{"foo":{"bar":"baz"}}` and the fragment part `#/foo/bar` results in digging the object to obtain the value at `$.foo.bar`. - ### File File provider reads a local text file, or the value for the specific path in a YAML/JSON file. diff --git a/go.mod b/go.mod index 07e6c6c..484834b 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/hashicorp/golang-lru v1.0.2 github.com/hashicorp/vault/api v1.15.0 + github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 github.com/stretchr/testify v1.10.0 github.com/tidwall/gjson v1.18.0 golang.org/x/oauth2 v0.24.0 diff --git a/pkg/providers/keychain/keychain.go b/pkg/providers/keychain/keychain.go new file mode 100644 index 0000000..7132ca0 --- /dev/null +++ b/pkg/providers/keychain/keychain.go @@ -0,0 +1,67 @@ +package keychain + +import ( + "errors" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/helmfile/vals/pkg/api" + "github.com/keybase/go-keychain" +) + +const keychainKind = "vals-secret" + +type provider struct { +} + +func New(cfg api.StaticConfig) *provider { + p := &provider{} + return p +} + +func getKeychainSecret(key string) ([]byte, error) { + query := keychain.NewItem() + query.SetSecClass(keychain.SecClassGenericPassword) + query.SetLabel(key) + query.SetDescription(keychainKind) + query.SetMatchLimit(keychain.MatchLimitOne) + query.SetReturnData(true) + + results, err := keychain.QueryItem(query) + if err != nil { + return nil, err + } else if len(results) == 0 { + return nil, errors.New("not found") + } + + return results[0].Data, nil +} + +func (p *provider) GetString(key string) (string, error) { + key = strings.TrimSuffix(key, "/") + key = strings.TrimSpace(key) + + secret, err := getKeychainSecret(key) + if err != nil { + return "", err + } + + return string(secret), err +} + +func (p *provider) GetStringMap(key string) (map[string]interface{}, error) { + key = strings.TrimSuffix(key, "/") + key = strings.TrimSpace(key) + + secret, err := getKeychainSecret(key) + if err != nil { + return nil, err + } + + m := map[string]interface{}{} + if err := yaml.Unmarshal(secret, &m); err != nil { + return nil, err + } + return m, nil +} diff --git a/vals.go b/vals.go index fdb23a7..54bc3de 100644 --- a/vals.go +++ b/vals.go @@ -37,6 +37,7 @@ import ( "github.com/helmfile/vals/pkg/providers/hcpvaultsecrets" "github.com/helmfile/vals/pkg/providers/httpjson" "github.com/helmfile/vals/pkg/providers/k8s" + "github.com/helmfile/vals/pkg/providers/keychain" "github.com/helmfile/vals/pkg/providers/onepassword" "github.com/helmfile/vals/pkg/providers/onepasswordconnect" "github.com/helmfile/vals/pkg/providers/pulumi" @@ -91,6 +92,7 @@ const ( ProviderTFStateRemote = "tfstateremote" ProviderAzureKeyVault = "azurekeyvault" ProviderEnvSubst = "envsubst" + ProviderKeychain = "keychain" ProviderOnePassword = "op" ProviderOnePasswordConnect = "onepasswordconnect" ProviderDoppler = "doppler" @@ -245,6 +247,9 @@ func (r *Runtime) prepare() (*expansion.ExpandRegexMatch, error) { case ProviderKms: p := awskms.New(conf) return p, nil + case ProviderKeychain: + p := keychain.New(conf) + return p, nil case ProviderEnvSubst: p := envsubst.New(conf) return p, nil @@ -491,6 +496,7 @@ var KnownValuesTypes = []string{ ProviderTFState, ProviderFile, ProviderEcho, + ProviderKeychain, ProviderEnvSubst, ProviderPulumiStateAPI, }