Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Encoding of Secrets #194

Merged
merged 3 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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) {
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 @@ -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",
Expand Down
27 changes: 26 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 @@ -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
Expand Down Expand Up @@ -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
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 @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" ]]

Expand Down