Skip to content

Commit a99cac2

Browse files
author
George Harley
committed
Create Vault client once then renew secret token as needed
* SecretStoreClient created exactly once as part of first reconciliation * Vault token owned by SecretStoreClient is continually renewed in a background thread that manages its lifecycle * Lifecycle management thread is cognizant of Vault token's max TTL (defaults to 32 days) and will force a fresh Vault login when reached
1 parent 59b8d08 commit a99cac2

File tree

4 files changed

+173
-51
lines changed

4 files changed

+173
-51
lines changed

internal/cluster_reference.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (c ClusterCredentials) Data(key string) ([]byte, bool) {
2727
return result, ok
2828
}
2929

30-
var SecretStoreClientInitializer = InitializeSecretStoreClient
30+
var SecretStoreClientProvider = GetSecretStoreClient
3131

3232
var (
3333
NoSuchRabbitmqClusterError = errors.New("RabbitmqCluster object does not exist")
@@ -64,8 +64,8 @@ func ParseRabbitmqClusterReference(ctx context.Context, c client.Client, rmq top
6464
var credentialsProvider CredentialsProvider
6565
svc := &corev1.Service{}
6666
if cluster.Spec.SecretBackend.Vault != nil && cluster.Spec.SecretBackend.Vault.DefaultUserPath != "" {
67-
// ask the configured secure store for the credentials available at the path retrived from the cluster resource
68-
secretStoreClient, err := SecretStoreClientInitializer(cluster.Spec.SecretBackend.Vault)
67+
// ask the configured secure store for the credentials available at the path retrieved from the cluster resource
68+
secretStoreClient, err := SecretStoreClientProvider(cluster.Spec.SecretBackend.Vault)
6969
if err != nil {
7070
return nil, nil, nil, fmt.Errorf("unable to create a client connection to secret store: %w", err)
7171
}
@@ -74,6 +74,7 @@ func ParseRabbitmqClusterReference(ctx context.Context, c client.Client, rmq top
7474
if err != nil {
7575
return nil, nil, nil, fmt.Errorf("unable to retrieve credentials from secret store: %w", err)
7676
}
77+
7778
credentialsProvider = credsProv
7879

7980
if err := c.Get(ctx, types.NamespacedName{Namespace: namespace, Name: cluster.ObjectMeta.Name}, svc); err != nil {

internal/cluster_reference_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,13 @@ var _ = Describe("ParseRabbitmqClusterReference", func() {
156156

157157
fakeSecretStoreClient = &internalfakes.FakeSecretStoreClient{}
158158
fakeSecretStoreClient.ReadCredentialsReturns(fakeCredentialsProvider, nil)
159-
internal.SecretStoreClientInitializer = func(vaultSpec *rabbitmqv1beta1.VaultSpec) (internal.SecretStoreClient, error) {
159+
internal.SecretStoreClientProvider = func(vaultSpec *rabbitmqv1beta1.VaultSpec) (internal.SecretStoreClient, error) {
160160
return fakeSecretStoreClient, nil
161161
}
162162
})
163163

164164
AfterEach(func() {
165-
internal.SecretStoreClientInitializer = internal.InitializeSecretStoreClient
165+
internal.SecretStoreClientProvider = internal.GetSecretStoreClient
166166
})
167167

168168
JustBeforeEach(func() {

internal/vault_reader.go

Lines changed: 115 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import (
44
"errors"
55
"fmt"
66
"os"
7+
"sync"
78

89
rabbitmqv1beta1 "github.com/rabbitmq/cluster-operator/api/v1beta1"
910

11+
ctrl "sigs.k8s.io/controller-runtime"
12+
1013
vault "github.com/hashicorp/vault/api"
1114
)
1215

@@ -38,9 +41,42 @@ type VaultClient struct {
3841
Reader SecretReader
3942
}
4043

41-
var ServiceAccountTokenReader = ReadServiceAccountToken
42-
var VaultClientTokenReader = ReadVaultClientToken
43-
var VaultAuthenticator = LoginToVault
44+
var ReadServiceAccountTokenFunc = ReadServiceAccountToken
45+
var ReadVaultClientSecretFunc = ReadVaultClientSecret
46+
var LoginToVaultFunc = LoginToVault
47+
48+
var createSecretStoreClientOnce sync.Once
49+
var SecretClient SecretStoreClient
50+
var SecretClientCreationError error
51+
52+
func GetSecretStoreClient(vaultSpec *rabbitmqv1beta1.VaultSpec) (SecretStoreClient, error) {
53+
createSecretStoreClientOnce.Do(InitializeClient(vaultSpec))
54+
return SecretClient, SecretClientCreationError
55+
}
56+
57+
func InitializeClient(vaultSpec *rabbitmqv1beta1.VaultSpec) func() {
58+
return func() {
59+
// VAULT_ADDR environment variable will be the address that pod uses to communicate with Vault.
60+
config := vault.DefaultConfig() // modify for more granular configuration
61+
vaultClient, err := vault.NewClient(config)
62+
if err != nil {
63+
SecretClientCreationError = fmt.Errorf("unable to initialize Vault client: %w", err)
64+
return
65+
}
66+
67+
_, err = login(vaultClient, vaultSpec)
68+
if err != nil {
69+
SecretClientCreationError = fmt.Errorf("unable to login to Vault: %w", err)
70+
return
71+
}
72+
73+
SecretClient = VaultClient{
74+
Reader: &VaultSecretReader{client: vaultClient},
75+
}
76+
77+
go renewToken(vaultClient, vaultSpec)
78+
}
79+
}
4480

4581
func (vc VaultClient) ReadCredentials(path string) (CredentialsProvider, error) {
4682
secret, err := vc.Reader.ReadSecret(path)
@@ -110,46 +146,105 @@ func availableKeys(m map[string]interface{}) []string {
110146
return result
111147
}
112148

113-
func InitializeSecretStoreClient(vaultSpec *rabbitmqv1beta1.VaultSpec) (SecretStoreClient, error) {
149+
func login(vaultClient *vault.Client, vaultSpec *rabbitmqv1beta1.VaultSpec) (*vault.Secret, error) {
150+
logger := ctrl.LoggerFrom(nil)
151+
114152
// GCH TODO return to this...
115153
// role := vaultSpec.Role
116154
// if role == "" {
117155
// return nil, errors.New("no role value set in Vault secret backend")
118156
// }
119157
role := "messaging-topology-operator"
120158

121-
// For now, the VAULT_ADDR environment variable will be the address that your pod uses to communicate with Vault.
122-
config := vault.DefaultConfig() // modify for more granular configuration
123-
124-
vaultClient, err := vault.NewClient(config)
125-
if err != nil {
126-
return nil, fmt.Errorf("unable to initialize Vault client: %w", err)
127-
}
128-
129159
var annotations = vaultSpec.Annotations
130160
if annotations["vault.hashicorp.com/namespace"] != "" {
131161
vaultClient.SetNamespace(annotations["vault.hashicorp.com/namespace"])
132162
}
133163

134-
jwt, err := ServiceAccountTokenReader()
164+
jwt, err := ReadServiceAccountTokenFunc()
135165
if err != nil {
136166
return nil, fmt.Errorf("unable to read file containing service account token: %w", err)
137167
}
138168

139169
loginAuthPath := defaultAuthPath
170+
annotations = vaultSpec.Annotations
140171
if annotations["vault.hashicorp.com/auth-path"] != "" {
141172
loginAuthPath = annotations["vault.hashicorp.com/auth-path"]
142173
}
143174

144-
vaultToken, err := VaultClientTokenReader(vaultClient, string(jwt), role, loginAuthPath)
175+
logger.Info("Authenticating to Vault")
176+
177+
vaultSecret, err := ReadVaultClientSecretFunc(vaultClient, string(jwt), role, loginAuthPath)
145178
if err != nil {
146-
return nil, fmt.Errorf("unable to read Vault client token: %w", err)
179+
return nil, fmt.Errorf("unable to obtain Vault client secret: %w", err)
180+
}
181+
182+
if vaultSecret == nil || vaultSecret.Auth == nil || vaultSecret.Auth.ClientToken == "" {
183+
return nil, fmt.Errorf("no client token found in Vault secret")
184+
}
185+
186+
vaultClient.SetToken(vaultSecret.Auth.ClientToken)
187+
return vaultSecret, nil
188+
}
189+
190+
func renewToken(client *vault.Client, vaultSpec *rabbitmqv1beta1.VaultSpec) {
191+
logger := ctrl.LoggerFrom(nil)
192+
193+
for {
194+
vaultLoginResp, err := login(client, vaultSpec)
195+
if err != nil {
196+
logger.Error(err, "unable to authenticate to Vault server")
197+
}
198+
199+
err = manageTokenLifecycle(client, vaultLoginResp)
200+
if err != nil {
201+
logger.Error(err, "unable to start managing the Vault token lifecycle")
202+
}
147203
}
204+
}
148205

149-
// use the Vault token for making all future calls to Vault
150-
vaultClient.SetToken(vaultToken)
206+
func manageTokenLifecycle(client *vault.Client, token *vault.Secret) error {
207+
logger := ctrl.LoggerFrom(nil)
208+
209+
if token == nil || token.Auth == nil {
210+
logger.Info("No Vault secret available. Re-attempting login")
211+
return nil
212+
}
213+
214+
renew := token.Auth.Renewable
215+
if !renew {
216+
logger.Info("Token is not configured to be renewable. Re-attempting login")
217+
return nil
218+
}
151219

152-
return VaultClient{Reader: &VaultSecretReader{client: vaultClient}}, nil
220+
watcher, err := client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
221+
Secret: token,
222+
})
223+
if err != nil {
224+
return fmt.Errorf("unable to initialize new lifetime watcher for renewing auth token: %w", err)
225+
}
226+
227+
go watcher.Start()
228+
defer watcher.Stop()
229+
230+
for {
231+
select {
232+
// `DoneCh` will return if renewal fails, or if the remaining lease duration is
233+
// under a built-in threshold and either renewing is not extending it or
234+
// renewing is disabled. In any case, the caller needs to attempt to log in again.
235+
case err := <-watcher.DoneCh():
236+
if err != nil {
237+
logger.Error(err, "Failed to renew Vault token. Re-attempting login")
238+
return nil
239+
}
240+
logger.Info("Token can no longer be renewed. Re-attempting login.")
241+
return nil
242+
243+
// Successfully completed renewal
244+
case renewal := <-watcher.RenewCh():
245+
logger.Info("Successfully renewed Vault token", "renewal info", renewal)
246+
}
247+
}
153248
}
154249

155250
func ReadServiceAccountToken() ([]byte, error) {
@@ -164,23 +259,13 @@ func ReadServiceAccountToken() ([]byte, error) {
164259
return token, nil
165260
}
166261

167-
func ReadVaultClientToken(vaultClient *vault.Client, jwtToken string, vaultRole string, authPath string) (string, error) {
262+
func ReadVaultClientSecret(vaultClient *vault.Client, jwtToken string, vaultRole string, authPath string) (*vault.Secret, error) {
168263
params := map[string]interface{}{
169264
"jwt": jwtToken,
170265
"role": vaultRole, // the name of the role in Vault that was created with this app's Kubernetes service account bound to it
171266
}
172267

173-
// log in to Vault's Kubernetes auth method
174-
resp, err := VaultAuthenticator(vaultClient, authPath, params)
175-
if err != nil {
176-
return "", fmt.Errorf("unable to log in with Kubernetes auth: %w", err)
177-
}
178-
179-
// return the Vault client token provided in the login response
180-
if resp == nil || resp.Auth == nil || resp.Auth.ClientToken == "" {
181-
return "", fmt.Errorf("no client token found in Vault login response")
182-
}
183-
return resp.Auth.ClientToken, nil
268+
return LoginToVaultFunc(vaultClient, authPath, params)
184269
}
185270

186271
func LoginToVault(vaultClient *vault.Client, authPath string, params map[string]interface{}) (*vault.Secret, error) {

internal/vault_reader_test.go

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,21 @@ var _ = Describe("VaultReader", func() {
290290
Describe("Initialize secret store client", func() {
291291
var (
292292
vaultSpec *rabbitmqv1beta1.VaultSpec
293+
getSecretStoreClientTester func(vaultSpec *rabbitmqv1beta1.VaultSpec)(internal.SecretStoreClient, error)
293294
)
294295

295296
When("vault spec has no role value", func() {
296297
BeforeEach(func() {
297298
vaultSpec = &rabbitmqv1beta1.VaultSpec{}
299+
300+
getSecretStoreClientTester = func(vaultSpec *rabbitmqv1beta1.VaultSpec) (internal.SecretStoreClient, error) {
301+
internal.InitializeClient(vaultSpec)()
302+
return internal.SecretClient, internal.SecretClientCreationError
303+
}
298304
})
299305

300306
JustBeforeEach(func() {
301-
secretStoreClient, err = internal.InitializeSecretStoreClient(vaultSpec)
307+
secretStoreClient, err = getSecretStoreClientTester(vaultSpec)
302308
})
303309

304310
PIt("should return a nil secret store client", func() {
@@ -313,13 +319,19 @@ var _ = Describe("VaultReader", func() {
313319

314320
When("service account token is not in the expected place", func() {
315321
BeforeEach(func() {
322+
internal.SecretClient = nil
323+
internal.SecretClientCreationError = nil
316324
vaultSpec = &rabbitmqv1beta1.VaultSpec{
317325
Role: "cheese-and-ham",
318326
}
327+
getSecretStoreClientTester = func(vaultSpec *rabbitmqv1beta1.VaultSpec) (internal.SecretStoreClient, error) {
328+
internal.InitializeClient(vaultSpec)()
329+
return internal.SecretClient, internal.SecretClientCreationError
330+
}
319331
})
320332

321333
JustBeforeEach(func() {
322-
secretStoreClient, err = internal.InitializeSecretStoreClient(vaultSpec)
334+
secretStoreClient, err = getSecretStoreClientTester(vaultSpec)
323335
})
324336

325337
It("should return a nil secret store client", func() {
@@ -332,26 +344,32 @@ var _ = Describe("VaultReader", func() {
332344
})
333345
})
334346

335-
When("unable to log into vault to obtain client token", func() {
347+
When("unable to log into vault to obtain client secret", func() {
336348
BeforeEach(func() {
349+
internal.SecretClient = nil
350+
internal.SecretClientCreationError = nil
337351
vaultSpec = &rabbitmqv1beta1.VaultSpec{
338352
Role: "cheese-and-ham",
339353
}
340-
internal.ServiceAccountTokenReader = func() ([]byte, error) {
354+
internal.ReadServiceAccountTokenFunc = func() ([]byte, error) {
341355
return []byte("token"), nil
342356
}
343-
internal.VaultAuthenticator = func(vaultClient *vault.Client, authPath string, params map[string]interface{}) (*vault.Secret, error) {
357+
internal.LoginToVaultFunc = func(vaultClient *vault.Client, authPath string, params map[string]interface{}) (*vault.Secret, error) {
344358
return nil, errors.New("login failed (quickly!)")
345359
}
360+
getSecretStoreClientTester = func(vaultSpec *rabbitmqv1beta1.VaultSpec) (internal.SecretStoreClient, error) {
361+
internal.InitializeClient(vaultSpec)()
362+
return internal.SecretClient, internal.SecretClientCreationError
363+
}
346364
})
347365

348366
AfterEach(func() {
349-
internal.ServiceAccountTokenReader = internal.ReadServiceAccountToken
350-
internal.VaultAuthenticator = internal.LoginToVault
367+
internal.ReadServiceAccountTokenFunc = internal.ReadServiceAccountToken
368+
internal.LoginToVaultFunc = internal.LoginToVault
351369
})
352370

353371
JustBeforeEach(func() {
354-
secretStoreClient, err = internal.InitializeSecretStoreClient(vaultSpec)
372+
secretStoreClient, err = getSecretStoreClientTester(vaultSpec)
355373
})
356374

357375
It("should return a nil secret store client", func() {
@@ -360,30 +378,48 @@ var _ = Describe("VaultReader", func() {
360378

361379
It("should have returned an error", func() {
362380
Expect(err).To(HaveOccurred())
363-
Expect(err.Error()).To(ContainSubstring("unable to log in with Kubernetes"))
381+
Expect(err.Error()).To(ContainSubstring("unable to obtain Vault client secret"))
364382
})
365383
})
366384

367-
When("client token obtained from vault", func() {
385+
When("client secret obtained from vault", func() {
368386
BeforeEach(func() {
387+
internal.SecretClient = nil
388+
internal.SecretClientCreationError = nil
369389
vaultSpec = &rabbitmqv1beta1.VaultSpec{
370390
Role: "cheese-and-ham",
371391
}
372-
internal.ServiceAccountTokenReader = func() ([]byte, error) {
392+
internal.ReadServiceAccountTokenFunc = func() ([]byte, error) {
373393
return []byte("token"), nil
374394
}
375-
internal.VaultClientTokenReader = func(vaultClient *vault.Client, jwtToken string, vaultRole string, authPath string) (string, error) {
376-
return "vault-token", nil
395+
internal.LoginToVaultFunc = func(vaultClient *vault.Client, authPath string, params map[string]interface{}) (*vault.Secret, error) {
396+
return &vault.Secret{
397+
Auth: &vault.SecretAuth{
398+
ClientToken: "vault-secret-token",
399+
},
400+
}, nil
401+
}
402+
internal.ReadVaultClientSecretFunc = func(vaultClient *vault.Client, jwtToken string, vaultRole string, authPath string) (*vault.Secret, error) {
403+
return &vault.Secret{
404+
Auth: &vault.SecretAuth{
405+
ClientToken: "vault-secret-token",
406+
},
407+
}, nil
408+
}
409+
getSecretStoreClientTester = func(vaultSpec *rabbitmqv1beta1.VaultSpec) (internal.SecretStoreClient, error) {
410+
internal.InitializeClient(vaultSpec)()
411+
return internal.SecretClient, internal.SecretClientCreationError
377412
}
378413
})
379414

380415
AfterEach(func() {
381-
internal.ServiceAccountTokenReader = internal.ReadServiceAccountToken
382-
internal.VaultClientTokenReader = internal.ReadVaultClientToken
416+
internal.ReadServiceAccountTokenFunc = internal.ReadServiceAccountToken
417+
internal.LoginToVaultFunc = internal.LoginToVault
418+
internal.ReadVaultClientSecretFunc = internal.ReadVaultClientSecret
383419
})
384420

385421
JustBeforeEach(func() {
386-
secretStoreClient, err = internal.InitializeSecretStoreClient(vaultSpec)
422+
secretStoreClient, err = getSecretStoreClientTester(vaultSpec)
387423
})
388424

389425
It("should not error", func() {

0 commit comments

Comments
 (0)