diff --git a/cert/source_test.go b/cert/source_test.go index 41943a13f..30c407f66 100644 --- a/cert/source_test.go +++ b/cert/source_test.go @@ -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"] } @@ -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) } diff --git a/cert/vault_source.go b/cert/vault_source.go index a847ad2e8..be326202f 100644 --- a/cert/vault_source.go +++ b/cert/vault_source.go @@ -5,6 +5,8 @@ import ( "crypto/x509" "fmt" "log" + "path" + "strings" "time" "github.com/hashicorp/vault/api" @@ -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 } @@ -72,9 +86,19 @@ 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) } @@ -82,17 +106,77 @@ func (s *VaultSource) load(path string) (pemBlocks map[string][]byte, err error) 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 +}