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

Feature/gh 618 vault dynamic secrets for gcp #632

Open
wants to merge 19 commits into
base: vault-dynamic-secrets
Choose a base branch
from
Open
Prev Previous commit
Next Next commit
refactoring hashivault secret management
ROSSO, LOIC committed Apr 10, 2020
commit 7bc58f989d0cabd3144383a1c844e58ef91060e7
116 changes: 111 additions & 5 deletions vault/hashivault/hashivault.go
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
package hashivault

import (
"encoding/base64"
"fmt"
"strings"

@@ -153,7 +154,23 @@ func (vc *vaultClient) GetSecret(id string, options ...string) (vault.Secret, er
if s == nil {
return nil, errors.Errorf("secret %q not found", id)
}
secret := &vaultSecret{Secret: s, options: opts}
// TODO: in the future combine this with mountPathDetection to see secret engine use
path := strings.SplitN(strings.TrimPrefix(strings.TrimSpace(id), "/"), "/", 2)
var secret vault.Secret
switch path[0] {
case "gcp":
secret = &gcpSecret{Secret: s, options: opts}
//return &gcpSecret{Secret: s, options: opts}, nil
case "secret":
// /secret/data/foo is a kv v2 path
if strings.HasPrefix(path[1], "data") {
secret = &kvV2Secret{Secret: s, options: opts}
} else {
secret = &kvV1Secret{Secret: s, options: opts}
}
default:
secret = &defaultSecret{Secret: s, options: opts}
}
return secret, nil
}

@@ -185,6 +202,20 @@ func (vc *vaultClient) startRenewing() {
}
}()
}

/* Vault users could register secret engine at different path. For example a gcp secret engine could be at /infra-prod/
So it will be better to know which path corresponds to which type to correctly manage secrets. TODO: In the future
func (vc *vaultClient) registerMounts()error{
mounts, err := vc.vClient.Sys().ListMounts()
if err != nil {
return errors.Errorf("Unable to list mounts. Err : %v", err)
}
for path, mount := range mounts {
type := mount.Type
}
}
*/

func (vc *vaultClient) Revoke(secret *api.Secret) error {
if secret.LeaseID == "" {
log.Debugf("Secret %v is non-revocable since it as no lease_id.", secret.WrapInfo.CreationPath)
@@ -201,30 +232,105 @@ func (vc *vaultClient) Shutdown() error {
return nil
}

// Default secret is the basic secret type, used type is not yet supported
type defaultSecret struct {
*api.Secret
options map[string]string
}

func (ds *defaultSecret) Raw() interface{} {
return ds.Secret
}

func (ds *defaultSecret) String() string {
if key, ok := ds.options["data"]; ok {
return fmt.Sprint(ds.Data[key])
}
return fmt.Sprint(ds.Data)
}

// Management of KV v1 secret engine
type kvV1Secret defaultSecret

func (kv1s *kvV1Secret) Raw() interface{} {
return kv1s.Secret
}

func (kv1s *kvV1Secret) String() string {
if key, ok := kv1s.options["data"]; ok {
return fmt.Sprint(kv1s.Data[key])
}
return fmt.Sprint(kv1s.Data)
}

// Management of KV v2 secret engine
type kvV2Secret defaultSecret

func (kv2s *kvV2Secret) Raw() interface{} {
return kv2s.Secret
}

func (kv2s *kvV2Secret) String() string {
if key, ok := kv2s.options["data"]; ok {
if secretValue, ok := getData(kv2s.Secret)[key]; ok {
return fmt.Sprint(secretValue)
}
}
return fmt.Sprint(getData(kv2s.Secret))
}

// Management of GCP secret engine
type gcpSecret defaultSecret

func (gcps *gcpSecret) Raw() interface{} {
return gcps.Secret
}

func (gcps *gcpSecret) String() string {
data := gcps.Data
if key, ok := gcps.options["data"]; ok {
switch key {
// Private key is base64 encoded
case "private_key_data":
decodedKey, err := base64.StdEncoding.DecodeString(fmt.Sprint(data[key]))
if err == nil {
return string(decodedKey)
}
break
default:
return fmt.Sprint(data[key])
}
}
return fmt.Sprint(data)
}

//DEPRECATED use defaultSecret type instead
type vaultSecret struct {
*api.Secret
options map[string]string
}

//DEPRECATED use defaultSecret type instead
func (vs *vaultSecret) String() string {
//Option exists
if key, ok := vs.options["data"]; ok {
if secretValue, ok := vs.getData()[key]; ok {
if secretValue, ok := getData(vs.Secret)[key]; ok {
return fmt.Sprint(secretValue)
}
}
return fmt.Sprint(vs.getData())
return fmt.Sprint(getData(vs.Secret))
}

//DEPRECATED use defaultSecret type instead
func (vs *vaultSecret) Raw() interface{} {
return vs.Secret
}

// In case of data nested into another data map return the actual data.
// KV secret engine has 2 version; version 2 manage secret versioning. For a KV v2, secret are sored in a struct like : map[data:map[k1: val1 k2: val2] metadata:map[created_time:2020 version:3]]
// For a KV v1, only the data map is stored
func (vs *vaultSecret) getData() map[string]interface{} {
data := vs.Data
func getData(secret *api.Secret) map[string]interface{} {
data := secret.Data
// Case of a KV v2 :
if nestedData, ok := data["data"].(map[string]interface{}); ok {
return nestedData
19 changes: 9 additions & 10 deletions vault/hashivault/hashivault_test.go
Original file line number Diff line number Diff line change
@@ -19,7 +19,6 @@ import (
"net"
"testing"

"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
"github.com/stretchr/testify/require"
@@ -29,6 +28,7 @@ import (

// Sample from https://www.vaultproject.io/api/secret/kv/kv-v1.html#sample-response
// and https://www.vaultproject.io/api/secret/kv/kv-v2.html#sample-response-1
// and https://www.vaultproject.io/api-docs/secret/gcp/#sample-response-4
var KVv1ReadSampleResponse = `
{
"auth": null,
@@ -60,14 +60,15 @@ var KVv2ReadsSampleResponse = `
}
`

// "<private-key-data>" encoded in base64 -> "PHByaXZhdGUta2V5LWRhdGE+Cg=="
var gcpReadSampleResponse = `
{
"request_id": "12345",
"lease_id": "gcp/key/my-key-roleset/9876",
"renewable": true,
"lease_duration": 3600,
"data": {
"private_key_data": "<private-key-data>",
"private_key_data": "PHByaXZhdGUta2V5LWRhdGE+",
"key_algorithm": "TYPE_GOOGLE_CREDENTIALS_FILE",
"key_type": "KEY_ALG_RSA_2048"
},
@@ -80,31 +81,29 @@ var gcpReadSampleResponse = `
func Test_vaultSecret_String(t *testing.T) {
tests := []struct {
sampleResponse string
options map[string]string
secret v.Secret
expected string
}{
{
KVv1ReadSampleResponse,
map[string]string{"data": "foo"},
&kvV1Secret{nil, map[string]string{"data": "foo"}},
"bar",
}, {
KVv2ReadsSampleResponse,
map[string]string{"data": "foo"},
&kvV2Secret{nil, map[string]string{"data": "foo"}},
"bar",
}, {
gcpReadSampleResponse,
map[string]string{"data": "private_key_data"},
&gcpSecret{nil, map[string]string{"data": "private_key_data"}},
"<private-key-data>",
},
}
for _, testCase := range tests {
var s api.Secret
err := json.Unmarshal([]byte(testCase.sampleResponse), &s)
err := json.Unmarshal([]byte(testCase.sampleResponse), testCase.secret)
if err != nil {
t.Fatal(err)
}
vs := &vaultSecret{&s, testCase.options}
require.Equal(t, vs.String(), testCase.expected, "It should retrieve the good value and not the entire map !")
require.Equal(t, testCase.expected, testCase.secret.String(), "String() should retrieve the good value of the data map !")
}

}