aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-05-21 13:04:49 -0700
committerMike Crute <mike@crute.us>2022-05-21 13:04:49 -0700
commit5bf75fb5b7e88153e34d2c7133315b654dbe1642 (patch)
tree5e02e16f485f9826dcdba419d0b40d42debbaf93
parentf4f23715cf22f06b7fb3b7663d054c20d220ce13 (diff)
downloadgolib-5bf75fb5b7e88153e34d2c7133315b654dbe1642.tar.bz2
golib-5bf75fb5b7e88153e34d2c7133315b654dbe1642.tar.xz
golib-5bf75fb5b7e88153e34d2c7133315b654dbe1642.zip
vault: add full client with renewalvault/v0.2.0
-rw-r--r--vault/client.go330
-rw-r--r--vault/go.mod7
-rw-r--r--vault/go.sum11
-rw-r--r--vault/simple_client.go21
4 files changed, 363 insertions, 6 deletions
diff --git a/vault/client.go b/vault/client.go
new file mode 100644
index 0000000..2f645d4
--- /dev/null
+++ b/vault/client.go
@@ -0,0 +1,330 @@
1package vault
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "path"
8 "sync"
9 "time"
10
11 "github.com/hashicorp/vault/api"
12 "github.com/hashicorp/vault/api/auth/approle"
13 "github.com/mitchellh/mapstructure"
14)
15
16type VaultClient interface {
17 LoginApprole(c context.Context, roleId string, secretId string) error
18
19 DbStaticCredential(c context.Context, suffix string) (*VaultUsernamePassword, error)
20 DbCredential(c context.Context, suffix string) (*VaultUsernamePassword, error)
21
22 KV(c context.Context, suffix string, out interface{}) (*VaultSecret, error)
23 KVApiKey(c context.Context, suffix string) (*VaultApiKey, error)
24 KVCredential(c context.Context, suffix string) (*VaultUsernamePassword, error)
25
26 Destroy(HasSecret)
27 Run(ctx context.Context, wg *sync.WaitGroup) error
28}
29
30type HasSecret interface {
31 VaultSecret() *VaultSecret
32}
33
34// VaultSecret is an opaque reference to a secret from Vault. It is
35// meant to be given to the Destroy function to check-in and destroy
36// unneeded credentials. Everything returned from the client has a
37// VaultSecret and implements HasSecret for that purpose. If the
38// credential is not renewable then destroying it is a no-op.
39type VaultSecret struct {
40 s *api.Secret
41 n string
42}
43
44func (s *VaultSecret) VaultSecret() *VaultSecret {
45 return s
46}
47
48type VaultApiKey struct {
49 Key string `json:"key"`
50 s *VaultSecret
51}
52
53func (k *VaultApiKey) VaultSecret() *VaultSecret {
54 return k.s
55}
56
57type VaultUsernamePassword struct {
58 Username string `json:"username"`
59 Password string `json:"password"`
60 s *VaultSecret
61}
62
63func (k *VaultUsernamePassword) VaultSecret() *VaultSecret {
64 return k.s
65}
66
67type Renewal struct {
68 RenewedAt time.Time
69 Name string
70}
71
72type vaultClient struct {
73 sync.Mutex
74 c *api.Client
75 lc *api.Logical
76 wg *sync.WaitGroup
77 watcherDone chan error
78 watchers map[string]*api.LifetimeWatcher
79 renewInfo chan *Renewal
80}
81
82// NewApproleClientEnv is a convenience function to create a new
83// VaultClient based on the environment, start it, and login using
84// Approle authentication.
85//
86// The following environment variables are used and must be present:
87//
88// VAULT_ADDR - URL to Vault server (of form https://host:port/)
89// VAULT_ROLE_ID - Role ID used for Approle authentication
90// VAULT_SECRET_ID - Secret ID used for Approle authentication
91//
92func NewApproleClientEnv(ctx context.Context, wg *sync.WaitGroup, renewInfo chan *Renewal) (VaultClient, error) {
93 vaultHost := os.Getenv("VAULT_ADDR")
94 if vaultHost == "" {
95 return nil, fmt.Errorf("NewApproleClientEnv: VAULT_ADDR is not set in environment")
96 }
97
98 roleId := os.Getenv("VAULT_ROLE_ID")
99 if roleId == "" {
100 return nil, fmt.Errorf("NewApproleClientEnv: VAULT_ROLE_ID is not set in environment")
101 }
102
103 secretId := os.Getenv("VAULT_SECRET_ID")
104 if secretId == "" {
105 return nil, fmt.Errorf("NewApproleClientEnv: VAULT_SECRET_ID is not set in environment")
106 }
107
108 vc, err := NewVaultClient(vaultHost, renewInfo)
109 if err != nil {
110 return nil, fmt.Errorf("NewApproleClientEnv: error creating client %w", err)
111 }
112
113 go vc.Run(ctx, wg)
114
115 if err = vc.LoginApprole(ctx, roleId, secretId); err != nil {
116 return nil, fmt.Errorf("NewApproleClientEnv: error logging in to vault %w", err)
117 }
118
119 return vc, nil
120}
121
122func NewVaultClient(host string, renewInfo chan *Renewal) (VaultClient, error) {
123 cfg := api.DefaultConfig()
124 cfg.Address = host
125
126 c, err := api.NewClient(cfg)
127 if err != nil {
128 return nil, err
129 }
130
131 return &vaultClient{
132 c: c,
133 lc: c.Logical(),
134 renewInfo: renewInfo,
135 watcherDone: make(chan error, 10),
136 watchers: map[string]*api.LifetimeWatcher{},
137 }, nil
138}
139
140func (c *vaultClient) watchWatcher(w *api.LifetimeWatcher, name string) {
141 c.wg.Add(1)
142 defer c.wg.Done()
143
144 for {
145 select {
146 case err := <-w.DoneCh():
147 if err != nil {
148 c.watcherDone <- err
149 }
150 return
151 case r := <-w.RenewCh():
152 // Report this so consumers can do their own reporting, if not
153 // provided we just read this to drain the chan and throw it away.
154 if c.renewInfo != nil {
155 c.renewInfo <- &Renewal{
156 Name: name,
157 RenewedAt: r.RenewedAt,
158 }
159 }
160 }
161 }
162}
163
164func (c *vaultClient) addWatcher(name string, s *api.Secret) error {
165 w, err := c.c.NewLifetimeWatcher(&api.LifetimeWatcherInput{
166 Secret: s,
167 })
168 if err != nil {
169 return err
170 }
171
172 c.Lock()
173 c.watchers[name] = w
174 c.Unlock()
175
176 go w.Start()
177 go c.watchWatcher(w, name)
178
179 return nil
180}
181
182func (c *vaultClient) read(ctx context.Context, prefix, suffix string) (*api.Secret, string, error) {
183 key := path.Join(prefix, suffix)
184
185 s, err := c.lc.ReadWithContext(ctx, key)
186 if err != nil {
187 return nil, "", err
188 }
189
190 if s.Renewable {
191 return s, key, c.addWatcher(key, s)
192 }
193
194 return s, key, nil
195}
196
197func (c *vaultClient) stop() {
198 c.Lock()
199 defer c.Unlock()
200
201 for _, w := range c.watchers {
202 w.Stop()
203 }
204}
205
206func (c *vaultClient) Run(ctx context.Context, wg *sync.WaitGroup) error {
207 c.Lock()
208 c.wg = wg
209 c.Unlock()
210
211 c.wg.Add(1)
212 defer c.wg.Done()
213
214 for {
215 select {
216 case <-ctx.Done():
217 c.stop()
218 return nil
219 case err := <-c.watcherDone:
220 c.stop()
221 return err
222 }
223 }
224}
225
226func (c *vaultClient) Destroy(s HasSecret) {
227 vs := s.VaultSecret()
228 if vs == nil || vs.n == "" || vs.s == nil {
229 return
230 }
231
232 c.Lock()
233 defer c.Unlock()
234
235 if w, ok := c.watchers[vs.n]; ok {
236 delete(c.watchers, vs.n)
237 w.Stop()
238 }
239
240 // TODO: Delete dynamic credentials like DB sessions from Vault
241
242 // Drop references to the secret so that even if the client holds on to
243 // it we free the RAM.
244 vs.s = nil
245 vs.n = ""
246}
247
248func (c *vaultClient) LoginApprole(ctx context.Context, roleId string, secretId string) error {
249 a, err := approle.NewAppRoleAuth(roleId, &approle.SecretID{FromString: secretId})
250 if err != nil {
251 return err
252 }
253
254 s, err := c.c.Auth().Login(ctx, a)
255 if err != nil {
256 return err
257 }
258
259 // This credential can not be destroyed like the others
260 return c.addWatcher("login", s)
261}
262
263func (c *vaultClient) DbStaticCredential(ctx context.Context, suffix string) (*VaultUsernamePassword, error) {
264 s, k, err := c.read(ctx, "database/static-creds", suffix)
265 if err != nil {
266 return nil, err
267 }
268
269 var d VaultUsernamePassword
270 if err = mapstructure.Decode(s.Data, &d); err != nil {
271 return nil, err
272 }
273
274 d.s = &VaultSecret{s: s, n: k}
275
276 return &d, nil
277}
278
279func (c *vaultClient) DbCredential(ctx context.Context, suffix string) (*VaultUsernamePassword, error) {
280 s, k, err := c.read(ctx, "database/creds", suffix)
281 if err != nil {
282 return nil, err
283 }
284
285 var d VaultUsernamePassword
286 if err = mapstructure.Decode(s.Data, &d); err != nil {
287 return nil, err
288 }
289
290 d.s = &VaultSecret{s: s, n: k}
291
292 return &d, nil
293}
294
295func (c *vaultClient) KV(ctx context.Context, suffix string, out interface{}) (*VaultSecret, error) {
296 s, k, err := c.read(ctx, "kv/data", suffix)
297 if err != nil {
298 return nil, err
299 }
300
301 if err = mapstructure.Decode(s.Data["data"], out); err != nil {
302 return nil, err
303 }
304
305 return &VaultSecret{s: s, n: k}, nil
306}
307
308func (c *vaultClient) KVApiKey(ctx context.Context, suffix string) (*VaultApiKey, error) {
309 var ak VaultApiKey
310 s, err := c.KV(ctx, suffix, &ak)
311 if err != nil {
312 return nil, err
313 }
314
315 ak.s = s
316
317 return &ak, nil
318}
319
320func (c *vaultClient) KVCredential(ctx context.Context, suffix string) (*VaultUsernamePassword, error) {
321 var ak VaultUsernamePassword
322 s, err := c.KV(ctx, suffix, &ak)
323 if err != nil {
324 return nil, err
325 }
326
327 ak.s = s
328
329 return &ak, nil
330}
diff --git a/vault/go.mod b/vault/go.mod
index 73fcdbe..05d59a0 100644
--- a/vault/go.mod
+++ b/vault/go.mod
@@ -3,7 +3,8 @@ module code.crute.us/mcrute/golib/vault
3go 1.17 3go 1.17
4 4
5require ( 5require (
6 github.com/hashicorp/vault/api v1.3.0 6 github.com/hashicorp/vault/api v1.5.0
7 github.com/hashicorp/vault/api/auth/approle v0.1.1
7 github.com/mitchellh/mapstructure v1.4.2 8 github.com/mitchellh/mapstructure v1.4.2
8) 9)
9 10
@@ -15,7 +16,7 @@ require (
15 github.com/golang/protobuf v1.5.2 // indirect 16 github.com/golang/protobuf v1.5.2 // indirect
16 github.com/golang/snappy v0.0.4 // indirect 17 github.com/golang/snappy v0.0.4 // indirect
17 github.com/hashicorp/errwrap v1.1.0 // indirect 18 github.com/hashicorp/errwrap v1.1.0 // indirect
18 github.com/hashicorp/go-cleanhttp v0.5.1 // indirect 19 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
19 github.com/hashicorp/go-hclog v0.16.2 // indirect 20 github.com/hashicorp/go-hclog v0.16.2 // indirect
20 github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 21 github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
21 github.com/hashicorp/go-multierror v1.1.1 // indirect 22 github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -30,7 +31,7 @@ require (
30 github.com/hashicorp/go-version v1.2.0 // indirect 31 github.com/hashicorp/go-version v1.2.0 // indirect
31 github.com/hashicorp/golang-lru v0.5.4 // indirect 32 github.com/hashicorp/golang-lru v0.5.4 // indirect
32 github.com/hashicorp/hcl v1.0.0 // indirect 33 github.com/hashicorp/hcl v1.0.0 // indirect
33 github.com/hashicorp/vault/sdk v0.3.0 // indirect 34 github.com/hashicorp/vault/sdk v0.4.1 // indirect
34 github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect 35 github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect
35 github.com/mattn/go-colorable v0.1.6 // indirect 36 github.com/mattn/go-colorable v0.1.6 // indirect
36 github.com/mattn/go-isatty v0.0.12 // indirect 37 github.com/mattn/go-isatty v0.0.12 // indirect
diff --git a/vault/go.sum b/vault/go.sum
index 7bbb973..c034144 100644
--- a/vault/go.sum
+++ b/vault/go.sum
@@ -89,8 +89,9 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
89github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 89github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
90github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 90github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
91github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 91github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
92github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
93github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 92github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
93github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
94github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
94github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 95github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
95github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= 96github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
96github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= 97github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs=
@@ -130,10 +131,14 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l
130github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 131github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
131github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 132github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
132github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 133github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
133github.com/hashicorp/vault/api v1.3.0 h1:uDy39PLSvy6gtKyjOCRPizy2QdFiIYSWBR2pxCEzYL8=
134github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ= 134github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ=
135github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY= 135github.com/hashicorp/vault/api v1.5.0 h1:Bp6yc2bn7CWkOrVIzFT/Qurzx528bdavF3nz590eu28=
136github.com/hashicorp/vault/api v1.5.0/go.mod h1:LkMdrZnWNrFaQyYYazWVn7KshilfDidgVBq6YiTq/bM=
137github.com/hashicorp/vault/api/auth/approle v0.1.1 h1:R5yA+xcNvw1ix6bDuWOaLOq2L4L77zDCVsethNw97xQ=
138github.com/hashicorp/vault/api/auth/approle v0.1.1/go.mod h1:mHOLgh//xDx4dpqXoq6tS8Ob0FoCFWLU2ibJ26Lfmag=
136github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= 139github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0=
140github.com/hashicorp/vault/sdk v0.4.1 h1:3SaHOJY687jY1fnB61PtL0cOkKItphrbLmux7T92HBo=
141github.com/hashicorp/vault/sdk v0.4.1/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0=
137github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= 142github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
138github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= 143github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
139github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 144github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
diff --git a/vault/simple_client.go b/vault/simple_client.go
index 4ceb4b5..2bef0a6 100644
--- a/vault/simple_client.go
+++ b/vault/simple_client.go
@@ -55,6 +55,27 @@ func GetVaultKeyStruct(path string, out interface{}) error {
55 return nil 55 return nil
56} 56}
57 57
58// GetVaultApiKey fetches a JSON k/v value from Vault. The JSON document
59// must have the format: { "key": "value" }
60func GetVaultApiKey(path string) (string, error) {
61 s, err := loginAndRead(fmt.Sprintf("kv/data/%s", path))
62 if err != nil {
63 return "", err
64 }
65
66 ret := struct {
67 Key string `json:"key"`
68 }{}
69 if err = mapstructure.Decode(s.Data["data"], &ret); err != nil {
70 return "", err
71 }
72
73 return ret.Key, nil
74}
75
76// GetVaultKey fetches a JSON k/v value from vault. The JSON document
77// must have the format:
78// { "username": "username", "password": "password" }
58func GetVaultKey(path string) (Credential, error) { 79func GetVaultKey(path string) (Credential, error) {
59 s, err := loginAndRead(fmt.Sprintf("kv/data/%s", path)) 80 s, err := loginAndRead(fmt.Sprintf("kv/data/%s", path))
60 if err != nil { 81 if err != nil {