Skip to content

Commit 4f15101

Browse files
committed
add support for KV v1 and v2
fixes #31
1 parent e26351c commit 4f15101

File tree

1 file changed

+95
-18
lines changed

1 file changed

+95
-18
lines changed

service/secret/vault/vault.go

+95-18
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package vault
22

33
import (
44
"context"
5-
"path/filepath"
5+
"path"
6+
"strings"
67
"time"
78

89
"github.com/hashicorp/go-cleanhttp"
@@ -17,36 +18,55 @@ import (
1718
type VaultSecrets struct {
1819
client *api.Client
1920
path string
21+
version int
2022
renewal time.Duration
2123
}
2224

2325
var _ secret.Store = &VaultSecrets{}
2426

2527
// New creates a new Vault client and pings the server
26-
func New(addr, path, token string, renewal time.Duration) (*VaultSecrets, error) {
27-
client, err := api.NewClient(&api.Config{
28+
func New(addr, basepath, token string, renewal time.Duration) (v *VaultSecrets, err error) {
29+
if strings.HasPrefix(basepath, "/") {
30+
basepath = basepath[1:]
31+
}
32+
33+
v = &VaultSecrets{
34+
path: basepath,
35+
renewal: renewal,
36+
}
37+
38+
if v.client, err = api.NewClient(&api.Config{
2839
Address: addr,
2940
HttpClient: cleanhttp.DefaultClient(),
30-
})
31-
if err != nil {
41+
}); err != nil {
3242
return nil, errors.Wrap(err, "failed to create vault client")
3343
}
34-
client.SetToken(token)
44+
v.client.SetToken(token)
3545

36-
if _, err = client.Auth().Token().LookupSelf(); err != nil {
46+
if _, err = v.client.Auth().Token().LookupSelf(); err != nil {
3747
return nil, errors.Wrap(err, "failed to connect to vault server")
3848
}
3949

40-
return &VaultSecrets{
41-
client: client,
42-
path: path,
43-
renewal: renewal,
44-
}, nil
50+
enginepath := strings.Split(basepath, "/")[0]
51+
if len(enginepath) == 0 {
52+
enginepath = basepath
53+
}
54+
55+
if v.version, err = getKVEngineVersion(v.client, enginepath); err != nil {
56+
return nil, errors.Wrapf(err, "failed to determine KV engine version at '/%s'", enginepath)
57+
}
58+
59+
zap.L().Debug("created new vault client for secrets engine",
60+
zap.Int("kv_version", v.version),
61+
zap.String("basepath", basepath),
62+
zap.String("enginepath", enginepath))
63+
64+
return v, nil
4565
}
4666

4767
// GetSecretsForTarget implements secret.Store
4868
func (v *VaultSecrets) GetSecretsForTarget(name string) (map[string]string, error) {
49-
path := filepath.Join(v.path, name)
69+
path := v.buildPath(name)
5070

5171
zap.L().Debug("looking for secrets in vault",
5272
zap.String("name", name),
@@ -63,14 +83,13 @@ func (v *VaultSecrets) GetSecretsForTarget(name string) (map[string]string, erro
6383
return nil, nil
6484
}
6585

66-
env := make(map[string]string)
67-
for k, v := range secret.Data {
68-
env[k] = v.(string)
86+
env, err := kvToMap(v.version, secret.Data)
87+
if err != nil {
88+
return nil, errors.Wrap(err, "failed to unwrap secret data")
6989
}
7090

7191
zap.L().Debug("found secrets in vault",
72-
zap.Any("secrets", env),
73-
zap.Int("count", len(env)))
92+
zap.Any("secret", secret))
7493

7594
return env, nil
7695
}
@@ -92,3 +111,61 @@ func (v *VaultSecrets) Renew(ctx context.Context) error {
92111
}
93112
return nil
94113
}
114+
115+
// builds the correct path to a secret based on the kv version
116+
func (v *VaultSecrets) buildPath(item string) string {
117+
if v.version == 1 {
118+
return path.Join(v.path, item)
119+
} else {
120+
return path.Join(v.path, "data", item)
121+
}
122+
}
123+
124+
// pulls out the kv secret data for v1 and v2 secrets
125+
func kvToMap(version int, data map[string]interface{}) (env map[string]string, err error) {
126+
if version == 1 {
127+
env = make(map[string]string)
128+
for k, v := range data {
129+
env[k] = v.(string)
130+
}
131+
} else if version == 2 {
132+
env = make(map[string]string)
133+
if kv, ok := data["data"].(map[string]interface{}); ok {
134+
for k, v := range kv {
135+
env[k] = v.(string)
136+
}
137+
} else {
138+
return nil, errors.New("could not interpret KV v2 response data as hashtable, this is likely a change in the KV v2 API, please open an issue.")
139+
}
140+
} else {
141+
return nil, errors.Errorf("unrecognised KV version: %d", version)
142+
}
143+
return
144+
}
145+
146+
// because Vault has no way to know if a kv engine is v1 or v2, we have to check
147+
// for the /config path and if it doesn't exist, attempt to LIST the path, if
148+
// that succeeds, it's a v1, if it doesn't succeed, it *might still* be a v1 but
149+
// empty, and in that case there's no way to know so it just bails. Amazing.
150+
func getKVEngineVersion(client *api.Client, basepath string) (int, error) {
151+
// only KV v2 has /config, /data, /metadata paths, so attempt to read one
152+
s, err := client.Logical().Read(path.Join(basepath, "config"))
153+
if err != nil {
154+
return 0, errors.Wrap(err, "failed to check engine config path for version query")
155+
}
156+
if s == nil {
157+
// no /config path present, now attempt to list the engine's base path
158+
l, err := client.Logical().List(path.Join(basepath))
159+
if err != nil {
160+
return 0, errors.Wrap(err, "failed to list possible KV v1 engine")
161+
}
162+
if l == nil {
163+
return 0, errors.New("could not read secrets engine, it's either an empty KV v1 engine or does not exist")
164+
}
165+
// engine does not have a /config but contains elements, it's a v1.
166+
return 1, nil
167+
}
168+
169+
// has a /config, it's a v2
170+
return 2, nil
171+
}

0 commit comments

Comments
 (0)