Skip to content
This repository was archived by the owner on Jul 26, 2022. It is now read-only.

Commit ab36718

Browse files
authored
fix(vault): Cache Vault clients/tokens on a per-role&mountpoint basis. (#488)
* Cache Vault clients/tokens on a per-role basis. * Cache not just by role but mountpoint too * Update vault-backend.js * Fix to vault-backend merge * Merge fix
1 parent 72920e4 commit ab36718

File tree

3 files changed

+210
-22
lines changed

3 files changed

+210
-22
lines changed

config/index.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,15 @@ if (envConfig.vaultNamespace) {
7676
'X-VAULT-NAMESPACE': envConfig.vaultNamespace
7777
}
7878
}
79-
const vaultClient = vault(vaultOptions)
79+
const vaultFactory = () => vault(vaultOptions)
80+
8081
// The Vault token is renewed only during polling, not asynchronously. The default tokenRenewThreshold
8182
// is three times larger than the pollerInterval so that the token is renewed before it
8283
// expires and with at least one remaining poll opportunty to retry renewal if it fails.
8384
const vaultTokenRenewThreshold = envConfig.vaultTokenRenewThreshold
8485
? Number(envConfig.vaultTokenRenewThreshold) : 3 * envConfig.pollerIntervalMilliseconds / 1000
8586
const vaultBackend = new VaultBackend({
86-
client: vaultClient,
87+
vaultFactory: vaultFactory,
8788
tokenRenewThreshold: vaultTokenRenewThreshold,
8889
logger: logger,
8990
defaultVaultMountPoint: envConfig.defaultVaultMountPoint,

lib/backends/vault-backend.js

+28-16
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ const KVBackend = require('./kv-backend')
66
class VaultBackend extends KVBackend {
77
/**
88
* Create Vault backend.
9-
* @param {Object} client - Client for interacting with Vault.
9+
* @param {Object} vaultFactory - arrow function to create a vault client.
1010
* @param {Number} tokenRenewThreshold - tokens are renewed when ttl reaches this threshold
1111
* @param {Object} logger - Logger for logging stuff.
1212
*/
13-
constructor ({ client, tokenRenewThreshold, logger, defaultVaultMountPoint, defaultVaultRole }) {
13+
constructor ({ vaultFactory, tokenRenewThreshold, logger, defaultVaultMountPoint, defaultVaultRole }) {
1414
super({ logger })
15-
this._client = client
15+
this._vaultFactory = vaultFactory
16+
this._clients = new Map()
1617
this._tokenRenewThreshold = tokenRenewThreshold
17-
this.defaultVaultMountPoint = defaultVaultMountPoint
18-
this.defaultVaultRole = defaultVaultRole
18+
this._defaultVaultMountPoint = defaultVaultMountPoint
19+
this._defaultVaultRole = defaultVaultRole
1920
}
2021

2122
/**
@@ -41,27 +42,38 @@ class VaultBackend extends KVBackend {
4142
* @returns {Promise} Promise object representing secret property values.
4243
*/
4344
async _get ({ key, specOptions: { vaultMountPoint = null, vaultRole = null, kvVersion = 2 } }) {
44-
if (!this._client.token) {
45+
const vaultMountPointGet = vaultMountPoint || this._defaultVaultMountPoint
46+
const vaultRoleGet = vaultRole || this._defaultVaultRole
47+
// Create cache key for auth specific client
48+
const clientCacheKey = `|m${vaultMountPointGet}|r${vaultRoleGet}|`
49+
// Lookup existing or create new vault client
50+
let client = this._clients.get(clientCacheKey)
51+
if (!client) {
52+
client = this._vaultFactory()
53+
this._clients.set(clientCacheKey, client)
54+
}
55+
56+
if (!client.token) {
4557
const jwt = this._fetchServiceAccountToken()
46-
this._logger.debug('fetching new token from vault')
47-
await this._client.kubernetesLogin({
48-
mount_point: vaultMountPoint || this.defaultVaultMountPoint,
49-
role: vaultRole || this.defaultVaultRole,
58+
this._logger.debug(`fetching new token from vault for role ${vaultRoleGet} on ${vaultMountPointGet}`)
59+
await client.kubernetesLogin({
60+
mount_point: vaultMountPointGet,
61+
role: vaultRoleGet,
5062
jwt: jwt
5163
})
5264
} else {
53-
this._logger.debug('checking vault token expiry')
54-
const tokenStatus = await this._client.tokenLookupSelf()
55-
this._logger.debug(`vault token valid for ${tokenStatus.data.ttl} seconds, renews at ${this._tokenRenewThreshold}`)
65+
this._logger.debug(`checking vault token expiry for role ${vaultRoleGet} on ${vaultMountPointGet}`)
66+
const tokenStatus = await client.tokenLookupSelf()
67+
this._logger.debug(`vault token (role ${vaultRoleGet} on ${vaultMountPointGet}) valid for ${tokenStatus.data.ttl} seconds, renews at ${this._tokenRenewThreshold}`)
5668

5769
if (Number(tokenStatus.data.ttl) <= this._tokenRenewThreshold) {
58-
this._logger.debug('renewing vault token')
59-
await this._client.tokenRenewSelf()
70+
this._logger.debug(`renewing role ${vaultRoleGet} on ${vaultMountPointGet} vault token`)
71+
await client.tokenRenewSelf()
6072
}
6173
}
6274

6375
this._logger.debug(`reading secret key ${key} from vault`)
64-
const secretResponse = await this._client.read(key)
76+
const secretResponse = await client.read(key)
6577

6678
if (kvVersion === 1) {
6779
return JSON.stringify(secretResponse.data)

lib/backends/vault-backend.test.js

+179-4
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ const logger = pino({
1414

1515
describe('VaultBackend', () => {
1616
let clientMock
17+
let clientMock2
1718
let vaultBackend
1819
const defaultFakeMountPoint = 'defaultFakeMountPoint'
1920
const defaultFakeRole = 'defaultFakeRole'
2021
const mountPoint = 'fakeMountPoint'
22+
const mountPoint2 = 'fakeMountPoint2'
2123
const role = 'fakeRole'
24+
const role2 = 'fakeRole2'
2225
const secretKey = 'fakeSecretKey'
2326
const secretValue = 'open, sesame'
2427
const secretData = { [secretKey]: secretValue }
@@ -50,9 +53,10 @@ describe('VaultBackend', () => {
5053

5154
beforeEach(() => {
5255
clientMock = sinon.mock()
53-
56+
clientMock2 = sinon.mock()
57+
let mock = 0
5458
vaultBackend = new VaultBackend({
55-
client: clientMock,
59+
vaultFactory: () => mock++ === 0 ? clientMock : clientMock2,
5660
tokenRenewThreshold: vaultTokenRenewThreshold,
5761
logger: logger,
5862
defaultVaultMountPoint: defaultFakeMountPoint,
@@ -63,6 +67,7 @@ describe('VaultBackend', () => {
6367
describe('_get', () => {
6468
beforeEach(() => {
6569
clientMock.read = sinon.stub().returns(kv2Secret)
70+
clientMock.token = undefined
6671
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
6772
clientMock.tokenRenewSelf = sinon.stub().returns(true)
6873
clientMock.kubernetesLogin = sinon.stub().returns({
@@ -71,9 +76,17 @@ describe('VaultBackend', () => {
7176
}
7277
})
7378

74-
vaultBackend._fetchServiceAccountToken = sinon.stub().returns(jwt)
79+
clientMock2.read = sinon.stub().returns(kv2Secret)
80+
clientMock2.token = undefined
81+
clientMock2.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
82+
clientMock2.tokenRenewSelf = sinon.stub().returns(true)
83+
clientMock2.kubernetesLogin = sinon.stub().returns({
84+
auth: {
85+
client_token: '5678'
86+
}
87+
})
7588

76-
clientMock.token = undefined
89+
vaultBackend._fetchServiceAccountToken = sinon.stub().returns(jwt)
7790
})
7891

7992
it('logs in and returns secret property value - default', async () => {
@@ -227,6 +240,168 @@ describe('VaultBackend', () => {
227240
// ... and expect to get its proper value
228241
expect(secretPropertyValue).equals(quotedSecretValue)
229242
})
243+
244+
it('returns secret property value using client associated with role given in _get', async () => {
245+
clientMock.read = sinon.stub().returns(kv2Secret)
246+
clientMock2.read = sinon.stub().returns(kv2Secret)
247+
248+
const secretPropertyValue = await vaultBackend._get({
249+
specOptions: {
250+
vaultMountPoint: mountPoint,
251+
vaultRole: role
252+
},
253+
key: secretKey
254+
})
255+
256+
// First, we log into Vault with role 1...
257+
sinon.assert.calledWith(clientMock.kubernetesLogin, {
258+
mount_point: mountPoint,
259+
role: role,
260+
jwt: jwt
261+
})
262+
263+
// ... then we fetch the secret ...
264+
sinon.assert.calledWith(clientMock.read, secretKey)
265+
266+
// ... and expect to get its proper value
267+
expect(secretPropertyValue).equals(quotedSecretValue)
268+
269+
const secretPropertyValue2 = await vaultBackend._get({
270+
specOptions: {
271+
vaultMountPoint: mountPoint,
272+
vaultRole: role2
273+
},
274+
key: secretKey
275+
})
276+
277+
// Now ensure we log into Vault with role 2...
278+
sinon.assert.calledWith(clientMock2.kubernetesLogin, {
279+
mount_point: mountPoint,
280+
role: role2,
281+
jwt: jwt
282+
})
283+
284+
// ... then we fetch the secret ...
285+
sinon.assert.calledWith(clientMock2.read, secretKey)
286+
287+
// ... and expect to get its proper value
288+
expect(secretPropertyValue2).equals(quotedSecretValue)
289+
})
290+
291+
it('returns secret property value using client associated with mountpoint given in _get', async () => {
292+
clientMock.read = sinon.stub().returns(kv2Secret)
293+
clientMock2.read = sinon.stub().returns(kv2Secret)
294+
295+
const secretPropertyValue = await vaultBackend._get({
296+
specOptions: {
297+
vaultMountPoint: mountPoint,
298+
vaultRole: role
299+
},
300+
key: secretKey
301+
})
302+
303+
// First, we log into Vault with mountpoint 1...
304+
sinon.assert.calledWith(clientMock.kubernetesLogin, {
305+
mount_point: mountPoint,
306+
role: role,
307+
jwt: jwt
308+
})
309+
310+
// ... then we fetch the secret ...
311+
sinon.assert.calledWith(clientMock.read, secretKey)
312+
313+
// ... and expect to get its proper value
314+
expect(secretPropertyValue).equals(quotedSecretValue)
315+
316+
const secretPropertyValue2 = await vaultBackend._get({
317+
specOptions: {
318+
vaultMountPoint: mountPoint2,
319+
vaultRole: role
320+
},
321+
key: secretKey
322+
})
323+
324+
// Now ensure we log into Vault with mountpoint 2...
325+
sinon.assert.calledWith(clientMock2.kubernetesLogin, {
326+
mount_point: mountPoint2,
327+
role: role,
328+
jwt: jwt
329+
})
330+
331+
// ... then we fetch the secret ...
332+
sinon.assert.calledWith(clientMock2.read, secretKey)
333+
334+
// ... and expect to get its proper value
335+
expect(secretPropertyValue2).equals(quotedSecretValue)
336+
})
337+
338+
it('ensure we cache the clients for each role', async () => {
339+
clientMock.read = sinon.stub().returns(kv2Secret)
340+
clientMock2.read = sinon.stub().returns(kv2Secret)
341+
342+
await vaultBackend._get({
343+
specOptions: {
344+
vaultMountPoint: mountPoint,
345+
vaultRole: role
346+
},
347+
key: secretKey
348+
})
349+
350+
// First, we log into Vault with role 1...
351+
sinon.assert.calledWith(clientMock.kubernetesLogin, {
352+
mount_point: mountPoint,
353+
role: role,
354+
jwt: jwt
355+
})
356+
sinon.assert.calledOnce(clientMock.read)
357+
358+
clientMock.token = 'an-existing-token'
359+
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultNoRenew)
360+
361+
// now we have active role 1 client, we now activate role 2 client
362+
363+
await vaultBackend._get({
364+
specOptions: {
365+
vaultMountPoint: mountPoint,
366+
vaultRole: role2
367+
},
368+
key: secretKey
369+
})
370+
371+
// we log into Vault with role 2...
372+
sinon.assert.calledWith(clientMock2.kubernetesLogin, {
373+
mount_point: mountPoint,
374+
role: role2,
375+
jwt: jwt
376+
})
377+
sinon.assert.calledOnce(clientMock2.read)
378+
379+
clientMock2.token = 'an-existing-token'
380+
clientMock2.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultNoRenew)
381+
382+
// Back to role1
383+
await vaultBackend._get({
384+
specOptions: {
385+
vaultMountPoint: mountPoint,
386+
vaultRole: role
387+
},
388+
key: secretKey
389+
})
390+
391+
sinon.assert.calledOnce(clientMock.kubernetesLogin)
392+
sinon.assert.calledTwice(clientMock.read)
393+
394+
await vaultBackend._get({
395+
specOptions: {
396+
vaultMountPoint: mountPoint,
397+
vaultRole: role2
398+
},
399+
key: secretKey
400+
})
401+
402+
sinon.assert.calledOnce(clientMock2.kubernetesLogin)
403+
sinon.assert.calledTwice(clientMock2.read)
404+
})
230405
})
231406

232407
describe('getSecretManifestData', () => {

0 commit comments

Comments
 (0)