From 6dd4a8887cdeb7413847318e5c9c95010b99320e Mon Sep 17 00:00:00 2001 From: Kristian Lebold Date: Sun, 12 Feb 2023 18:23:36 +0000 Subject: [PATCH 1/3] support encoding of secrets --- CHANGELOG.md | 4 ++ internal/config/config.go | 1 + internal/config/config_test.go | 8 +-- internal/provider/provider.go | 27 ++++++- internal/provider/provider_test.go | 71 ++++++++++++++----- .../vault-kv-sync-secretproviderclass.yaml | 6 ++ test/bats/provider.bats | 5 ++ 7 files changed, 99 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 528060e..c4b9a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +CHANGES: + +* Support utf-8 (default), hex, and base64 encoded secrets + ## 1.2.1 (November 21st, 2022) CHANGES: diff --git a/internal/config/config.go b/internal/config/config.go index ae0d2ef..e250372 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -90,6 +90,7 @@ type Secret struct { Method string `yaml:"method,omitempty"` SecretArgs map[string]interface{} `yaml:"secretArgs,omitempty"` FilePermission os.FileMode `yaml:"filePermission,omitempty"` + Encoding string `yaml:"encoding,omitempty"` } func Parse(parametersStr, targetPath, permissionStr string) (Config, error) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index fbd373f..a696728 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -96,8 +96,8 @@ func TestParseParameters(t *testing.T) { Insecure: true, }, Secrets: []Secret{ - {"bar1", "v1/secret/foo1", "", http.MethodGet, nil, 0}, - {"bar2", "v1/secret/foo2", "", "", nil, 0}, + {"bar1", "v1/secret/foo1", "", http.MethodGet, nil, 0, ""}, + {"bar2", "v1/secret/foo2", "", "", nil, 0, ""}, }, PodInfo: PodInfo{ Name: "nginx-secrets-store-inline", @@ -135,7 +135,7 @@ func TestParseConfig(t *testing.T) { expected.VaultRoleName = roleName expected.VaultTLSConfig.Insecure = true expected.Secrets = []Secret{ - {"bar1", "v1/secret/foo1", "", "", nil, 0o600}, + {"bar1", "v1/secret/foo1", "", "", nil, 0o600, ""}, } return expected }(), @@ -171,7 +171,7 @@ func TestParseConfig(t *testing.T) { VaultNamespace: "my-vault-namespace", VaultKubernetesMountPath: "my-mount-path", Secrets: []Secret{ - {"bar1", "v1/secret/foo1", "", "", nil, 0o600}, + {"bar1", "v1/secret/foo1", "", "", nil, 0o600, ""}, }, VaultTLSConfig: api.TLSConfig{ CACert: "my-ca-cert-path", diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f941772..fb1c52e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -7,6 +7,7 @@ import ( "context" "crypto/sha256" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "net/http" @@ -49,6 +50,12 @@ type cacheKey struct { method string } +const ( + EncodingBase64 string = "base64" + EncodingHex string = "hex" + EncodingUtf8 string = "utf-8" +) + func (p *provider) createJWTToken(ctx context.Context, podInfo config.PodInfo, audience string) (string, error) { p.logger.Debug("creating service account token bound to pod", "namespace", podInfo.Namespace, @@ -186,6 +193,19 @@ func keyFromData(rootData map[string]interface{}, secretKey string) ([]byte, err return nil, fmt.Errorf("failed to extract secret content as string or JSON from key %q", secretKey) } +func decodeValue(data []byte, encoding string) ([]byte, error) { + + if len(encoding) == 0 || strings.EqualFold(encoding, EncodingUtf8) { + return data, nil + } else if strings.EqualFold(encoding, EncodingBase64) { + return base64.StdEncoding.DecodeString(string(data)) + } else if strings.EqualFold(encoding, EncodingHex) { + return hex.DecodeString(string(data)) + } + + return nil, fmt.Errorf("invalid encoding type. Should be utf-8, base64, or hex") +} + func (p *provider) getSecret(ctx context.Context, client *api.Client, secretConfig config.Secret) ([]byte, error) { var secret *api.Secret var cached bool @@ -233,7 +253,12 @@ func (p *provider) getSecret(ctx context.Context, client *api.Client, secretConf return nil, fmt.Errorf("{%s}: {%w}", secretConfig.SecretPath, err) } - return value, nil + decodedVal, decodeErr := decodeValue(value, secretConfig.Encoding) + if decodeErr != nil { + return nil, fmt.Errorf("{%s}: {%w}", secretConfig.SecretPath, decodeErr) + } + + return decodedVal, nil } // MountSecretsStoreObjectContent mounts content of the vault object to target path diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 43e1a10..8180849 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -5,6 +5,7 @@ package provider import ( "context" + "encoding/base64" "encoding/json" "fmt" "net/http" @@ -238,15 +239,6 @@ func TestKeyFromDataMissingKey(t *testing.T) { } func TestHandleMountRequest(t *testing.T) { - // SETUP - mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandler())) - defer mockVaultServer.Close() - - k8sClient := fake.NewSimpleClientset( - &corev1.ServiceAccount{}, - &authenticationv1.TokenRequest{}, - ) - spcConfig := config.Config{ TargetPath: "some/unused/path", FilePermission: 0, @@ -259,6 +251,7 @@ func TestHandleMountRequest(t *testing.T) { SecretKey: "the-key", Method: "", SecretArgs: nil, + Encoding: "", }, { ObjectName: "object-two", @@ -266,13 +259,19 @@ func TestHandleMountRequest(t *testing.T) { SecretKey: "", Method: "", SecretArgs: nil, + Encoding: "", + }, + { + ObjectName: "object-three", + SecretPath: "path/three", + SecretKey: "the-key", + Method: "", + SecretArgs: nil, + Encoding: "base64", }, }, }, } - flagsConfig := config.FlagsConfig{ - VaultAddr: mockVaultServer.URL, - } // TEST expectedFiles := []*pb.File{ @@ -286,18 +285,50 @@ func TestHandleMountRequest(t *testing.T) { Mode: 0, Contents: []byte(`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":{"the-key":"secret v1 from: /v1/path/two"},"warnings":null}`), }, + { + Path: "object-three", + Mode: 0, + Contents: []byte("secret v1 from: /v1/path/three"), + }, } expectedVersions := []*pb.ObjectVersion{ { Id: "object-one", - Version: "NUAYElpND6QqTB7MYXdP_kCAjsXQTxCO24ExLXXsKPk=", + Version: "7eM6I4jvRmoPuY8XiQsUuJtEVDQlSE5JCPbXQWXN2tE=", }, { Id: "object-two", - Version: "2x2gbSKY0Ot2c9RW8djcD9o3oGuSbwZ1ZXzvnp8ArZg=", + Version: "V7eu3GtXFYYNJkbDDEfTNalWWpZl-VTu3Pu-qF9sWi4=", + }, + { + Id: "object-three", + Version: "95O8POIdARplTKNAtExps-7jm8jETgDB4idsUA9KcL8=", }, } + // SETUP + mockVaultServer := httptest.NewServer(http.HandlerFunc(mockVaultHandler( + map[string]func(numberOfCalls int) (string, interface{}){ + "/v1/path/one": func(numberOfCalls int) (string, interface{}) { + return "the-key", fmt.Sprintf("secret v%d from: /v1/path/one", numberOfCalls) + }, + "/v1/path/two": func(numberOfCalls int) (string, interface{}) { + return "the-key", fmt.Sprintf("secret v%d from: /v1/path/two", numberOfCalls) + }, + "/v1/path/three": func(numberOfCalls int) (string, interface{}) { + return "the-key", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("secret v%d from: /v1/path/three", numberOfCalls))) + }, + }, + ))) + flagsConfig := config.FlagsConfig{ + VaultAddr: mockVaultServer.URL, + } + defer mockVaultServer.Close() + + k8sClient := fake.NewSimpleClientset( + &corev1.ServiceAccount{}, + &authenticationv1.TokenRequest{}, + ) // While we hit the cache, the secret contents and versions should remain the same. provider := NewProvider(hclog.Default(), k8sClient) for i := 0; i < 3; i++ { @@ -317,13 +348,15 @@ func TestHandleMountRequest(t *testing.T) { assert.Equal(t, (*v1alpha1.Error)(nil), resp.Error) expectedFiles[0].Contents = []byte("secret v2 from: /v1/path/one") expectedFiles[1].Contents = []byte(`{"request_id":"","lease_id":"","lease_duration":0,"renewable":false,"data":{"the-key":"secret v2 from: /v1/path/two"},"warnings":null}`) - expectedVersions[0].Version = "_MwvWQq78rNEsiDtzGPtECvgHmCi2EhlXc6Sdmcemhw=" - expectedVersions[1].Version = "9Ck2wFZxO5vGIY08Pk_RNSfR8dJh-_QB4ev3KSCDXOg=" + expectedFiles[2].Contents = []byte("secret v2 from: /v1/path/three") + expectedVersions[0].Version = "R-NY6w6nGg5vX510c7i28A5sLZtxlDbu8y9zY92AUPY=" + expectedVersions[1].Version = "6hCb1c_dfqXbIdYYh7zEuqSG_f8ROpuE_5OmSja5pIk=" + expectedVersions[2].Version = "rKthxBOUCu5jDLuU6ZwabWnN4OWOiSPG8cnT2PtHqik=" assert.Equal(t, expectedFiles, resp.Files) assert.Equal(t, expectedVersions, resp.ObjectVersion) } -func mockVaultHandler() func(w http.ResponseWriter, req *http.Request) { +func mockVaultHandler(pathMapping map[string]func(numberOfCalls int) (string, interface{})) func(w http.ResponseWriter, req *http.Request) { getsPerPath := map[string]int{} return func(w http.ResponseWriter, req *http.Request) { @@ -348,9 +381,11 @@ func mockVaultHandler() func(w http.ResponseWriter, req *http.Request) { // Assume all GETs are secret reads and return a derivative of the request path. path := req.URL.Path getsPerPath[path]++ + mappingFunc := pathMapping[path] + key, value := mappingFunc(getsPerPath[path]) body, err := json.Marshal(&api.Secret{ Data: map[string]interface{}{ - "the-key": fmt.Sprintf("secret v%d from: %s", getsPerPath[path], path), + key: value, }, }) if err != nil { diff --git a/test/bats/configs/vault-kv-sync-secretproviderclass.yaml b/test/bats/configs/vault-kv-sync-secretproviderclass.yaml index 86c55f3..ae86901 100644 --- a/test/bats/configs/vault-kv-sync-secretproviderclass.yaml +++ b/test/bats/configs/vault-kv-sync-secretproviderclass.yaml @@ -18,6 +18,8 @@ spec: key: pwd - objectName: secret-2 key: username + - objectName: secret-3 + key: username_b64 parameters: roleName: "kv-role" vaultAddress: https://vault:8200 @@ -31,3 +33,7 @@ spec: - objectName: "secret-2" secretPath: "v1/secret/data/kv-sync2" secretKey: "bar2" + - objectName: "secret-3" + secretPath: "/v1/secret/data/kv-sync3" + secretKey: "bar3" + encoding: "base64" \ No newline at end of file diff --git a/test/bats/provider.bats b/test/bats/provider.bats index 8c8d955..b0a8f09 100644 --- a/test/bats/provider.bats +++ b/test/bats/provider.bats @@ -88,6 +88,7 @@ setup(){ kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv2 bar2=hello2 kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv-sync1 bar1=hello-sync1 kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv-sync2 bar2=hello-sync2 + kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv-sync3 bar3=aGVsbG8tc3luYzM= kubectl --namespace=csi exec vault-0 -- vault secrets enable -namespace=acceptance -path=secret -version=2 kv kubectl --namespace=csi exec vault-0 -- vault kv put -namespace=acceptance secret/kv1-namespace greeting=hello-namespaces kubectl --namespace=csi exec vault-0 -- vault kv put secret/kv-custom-audience bar=hello-custom-audience @@ -135,6 +136,7 @@ teardown(){ kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv-custom-audience kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv-sync1 kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv-sync2 + kubectl --namespace=csi exec vault-0 -- vault kv delete secret/kv-sync3 # Teardown shared k8s resources. kubectl delete --ignore-not-found namespace test @@ -185,6 +187,9 @@ teardown(){ result=$(kubectl --namespace=test exec $POD -- printenv | grep SECRET_USERNAME | awk -F"=" '{ print $2 }' | tr -d '\r\n') [[ "$result" == "hello-sync2" ]] + result=$(kubectl --namespace=test get secret kvsecret -o jsonpath="{.data.username_b64}" | base64 -d) + [[ "$result" == "hello-sync3" ]] + result=$(kubectl --namespace=test get secret kvsecret -o jsonpath="{.metadata.labels.environment}") [[ "${result//$'\r'}" == "test" ]] From a80c80d4b8a654a40f5b7ac87972980a925b567a Mon Sep 17 00:00:00 2001 From: Kristian Lebold Date: Sun, 12 Feb 2023 18:59:11 +0000 Subject: [PATCH 2/3] add reference to pr in readme --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b9a21..1a8193f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ CHANGES: -* Support utf-8 (default), hex, and base64 encoded secrets +* Support utf-8 (default), hex, and base64 encoded secrets [[GH-194](https://github.com/hashicorp/vault-csi-provider/pull/194)] ## 1.2.1 (November 21st, 2022) From be11cd24768cbf746cae1f1c5ab722724d98a212 Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Mon, 20 Mar 2023 17:09:25 +0000 Subject: [PATCH 3/3] Fix lint failure --- internal/provider/provider.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index fb1c52e..cadaf65 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -194,7 +194,6 @@ func keyFromData(rootData map[string]interface{}, secretKey string) ([]byte, err } func decodeValue(data []byte, encoding string) ([]byte, error) { - if len(encoding) == 0 || strings.EqualFold(encoding, EncodingUtf8) { return data, nil } else if strings.EqualFold(encoding, EncodingBase64) {