@@ -2,7 +2,8 @@ package vault
2
2
3
3
import (
4
4
"context"
5
- "path/filepath"
5
+ "path"
6
+ "strings"
6
7
"time"
7
8
8
9
"github.com/hashicorp/go-cleanhttp"
@@ -17,36 +18,55 @@ import (
17
18
type VaultSecrets struct {
18
19
client * api.Client
19
20
path string
21
+ version int
20
22
renewal time.Duration
21
23
}
22
24
23
25
var _ secret.Store = & VaultSecrets {}
24
26
25
27
// 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 {
28
39
Address : addr ,
29
40
HttpClient : cleanhttp .DefaultClient (),
30
- })
31
- if err != nil {
41
+ }); err != nil {
32
42
return nil , errors .Wrap (err , "failed to create vault client" )
33
43
}
34
- client .SetToken (token )
44
+ v . client .SetToken (token )
35
45
36
- if _ , err = client .Auth ().Token ().LookupSelf (); err != nil {
46
+ if _ , err = v . client .Auth ().Token ().LookupSelf (); err != nil {
37
47
return nil , errors .Wrap (err , "failed to connect to vault server" )
38
48
}
39
49
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
45
65
}
46
66
47
67
// GetSecretsForTarget implements secret.Store
48
68
func (v * VaultSecrets ) GetSecretsForTarget (name string ) (map [string ]string , error ) {
49
- path := filepath . Join ( v . path , name )
69
+ path := v . buildPath ( name )
50
70
51
71
zap .L ().Debug ("looking for secrets in vault" ,
52
72
zap .String ("name" , name ),
@@ -63,14 +83,13 @@ func (v *VaultSecrets) GetSecretsForTarget(name string) (map[string]string, erro
63
83
return nil , nil
64
84
}
65
85
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" )
69
89
}
70
90
71
91
zap .L ().Debug ("found secrets in vault" ,
72
- zap .Any ("secrets" , env ),
73
- zap .Int ("count" , len (env )))
92
+ zap .Any ("secret" , secret ))
74
93
75
94
return env , nil
76
95
}
@@ -92,3 +111,61 @@ func (v *VaultSecrets) Renew(ctx context.Context) error {
92
111
}
93
112
return nil
94
113
}
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