Skip to content

Commit

Permalink
Support Encoding of Secrets (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
BroCanDo authored Mar 20, 2023
1 parent 1e60bc5 commit 4571141
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 23 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

CHANGES:

* 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)

CHANGES:
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,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) {
Expand Down
8 changes: 4 additions & 4 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
}(),
Expand Down Expand Up @@ -172,7 +172,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",
Expand Down
26 changes: 25 additions & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -191,6 +198,18 @@ 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
Expand Down Expand Up @@ -238,7 +257,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
Expand Down
71 changes: 53 additions & 18 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package provider

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
Expand Down Expand Up @@ -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,
Expand All @@ -259,20 +251,27 @@ func TestHandleMountRequest(t *testing.T) {
SecretKey: "the-key",
Method: "",
SecretArgs: nil,
Encoding: "",
},
{
ObjectName: "object-two",
SecretPath: "path/two",
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{
Expand All @@ -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++ {
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions test/bats/configs/vault-kv-sync-secretproviderclass.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
5 changes: 5 additions & 0 deletions test/bats/provider.bats
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,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
Expand Down Expand Up @@ -140,6 +141,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
Expand Down Expand Up @@ -190,6 +192,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" ]]

Expand Down

0 comments on commit 4571141

Please sign in to comment.