Skip to content

Commit

Permalink
Make VaultSource compatible with KV Backend V2
Browse files Browse the repository at this point in the history
Detect if a KV backend uses the new v2 versioning features and rewrite
request paths and bodies if necessary.

The new API uses additional /data/ and /metadata/ for GET/PUT and LIST
operations, respectively. To facilitate versioning, v2 wraps the actual
payload in a JSON object with "data" and "metadata" keys:

    {
      "data": {<payload (same as v1)>},
      "metadata": { <versioning info> }
    }
  • Loading branch information
pschultz committed May 17, 2018
1 parent 1fb35b1 commit 4281606
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 8 deletions.
30 changes: 29 additions & 1 deletion cert/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,15 @@ func vaultServer(t *testing.T, addr, rootToken string) (*exec.Cmd, *vaultapi.Cli
capabilities = ["read"]
}
# Vault >= 0.10. (KV Version 2)
path "secret/metadata/fabio/cert/" {
capabilities = ["list"]
}
path "secret/data/fabio/cert/*" {
capabilities = ["read"]
}
path "test-pki/issue/fabio" {
capabilities = ["update"]
}
Expand Down Expand Up @@ -425,7 +434,26 @@ func TestVaultSource(t *testing.T) {
// create a cert and store it in vault
certPEM, keyPEM := makePEM("localhost", time.Minute)
data := map[string]interface{}{"cert": string(certPEM), "key": string(keyPEM)}
if _, err := client.Logical().Write(certPath+"/localhost", data); err != nil {

var nilSource *VaultSource // for calling helper methods

mountPath, v2, err := nilSource.isKVv2(certPath, client)
if err != nil {
t.Fatal(err)
}

p := certPath + "/localhost"
if v2 {
t.Log("Vault: KV backend: V2")
data = map[string]interface{}{
"data": data,
"options": map[string]interface{}{},
}
p = nilSource.addPrefixToVKVPath(p, mountPath, "data")
} else {
t.Log("Vault: KV backend: V1")
}
if _, err := client.Logical().Write(p, data); err != nil {
t.Fatalf("logical.Write failed: %s", err)
}

Expand Down
98 changes: 91 additions & 7 deletions cert/vault_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"crypto/x509"
"fmt"
"log"
"path"
"strings"
"time"

"github.com/hashicorp/vault/api"
Expand Down Expand Up @@ -47,8 +49,20 @@ func (s *VaultSource) load(path string) (pemBlocks map[string][]byte, err error)
// they are recognized by the post-processing function
// which assembles the certificates.
// The value can be stored either as string or []byte.
get := func(name, typ string, secret *api.Secret) {
v := secret.Data[typ]
get := func(name, typ string, secret *api.Secret, v2 bool) {
data := secret.Data
if v2 {
x, ok := secret.Data["data"]
if !ok {
return
}
data, ok = x.(map[string]interface{})
if !ok {
return
}
}

v := data[typ]
if v == nil {
return
}
Expand All @@ -72,27 +86,97 @@ func (s *VaultSource) load(path string) (pemBlocks map[string][]byte, err error)
return nil, fmt.Errorf("vault: client: %s", err)
}

mountPath, v2, err := s.isKVv2(path, c)
if err != nil {
return nil, fmt.Errorf("vault: query mount path: %s", err)
}

// get the subkeys under 'path'.
// Each subkey refers to a certificate.
certs, err := c.Logical().List(path)
p := path
if v2 {
p = s.addPrefixToVKVPath(p, mountPath, "metadata")
}

certs, err := c.Logical().List(p)
if err != nil {
return nil, fmt.Errorf("vault: list: %s", err)
}
if certs == nil || certs.Data["keys"] == nil {
return nil, nil
}

for _, s := range certs.Data["keys"].([]interface{}) {
name := s.(string)
for _, x := range certs.Data["keys"].([]interface{}) {
name := x.(string)
p := path + "/" + name
if v2 {
p = s.addPrefixToVKVPath(p, mountPath, "data")
}
secret, err := c.Logical().Read(p)
if err != nil {
log.Printf("[WARN] cert: Failed to read %s from Vault: %s", p, err)
continue
}
get(name, "cert", secret)
get(name, "key", secret)
get(name, "cert", secret, v2)
get(name, "key", secret, v2)
}

return pemBlocks, nil
}

func (s *VaultSource) addPrefixToVKVPath(p, mountPath, apiPrefix string) string {
p = strings.TrimPrefix(p, mountPath)
return path.Join(mountPath, apiPrefix, p)
}

func (s *VaultSource) isKVv2(path string, client *api.Client) (string, bool, error) {
mountPath, version, err := s.kvPreflightVersionRequest(client, path)
if err != nil {
return "", false, err
}

return mountPath, version == 2, nil
}

func (s *VaultSource) kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) {
r := client.NewRequest("GET", "/v1/sys/internal/ui/mounts/"+path)
resp, err := client.RawRequest(r)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
// If we get a 404 we are using an older version of vault, default to
// version 1
if resp != nil && resp.StatusCode == 404 {
return "", 1, nil
}

return "", 0, err
}

secret, err := api.ParseSecret(resp.Body)
if err != nil {
return "", 0, err
}
var mountPath string
if mountPathRaw, ok := secret.Data["path"]; ok {
mountPath = mountPathRaw.(string)
}
options := secret.Data["options"]
if options == nil {
return mountPath, 1, nil
}
versionRaw := options.(map[string]interface{})["version"]
if versionRaw == nil {
return mountPath, 1, nil
}
version := versionRaw.(string)
switch version {
case "", "1":
return mountPath, 1, nil
case "2":
return mountPath, 2, nil
}

return mountPath, 1, nil
}

0 comments on commit 4281606

Please sign in to comment.