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

Commit c3c27bc

Browse files
authored
fix(vault): handle token renewal failures (#497)
* Cache Vault clients/tokens on a per-role basis. * Clear vault token if errors when renewing or self-inspecting
1 parent ab36718 commit c3c27bc

File tree

3 files changed

+112
-9
lines changed

3 files changed

+112
-9
lines changed

config/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ const vaultFactory = () => vault(vaultOptions)
8383
// expires and with at least one remaining poll opportunty to retry renewal if it fails.
8484
const vaultTokenRenewThreshold = envConfig.vaultTokenRenewThreshold
8585
? Number(envConfig.vaultTokenRenewThreshold) : 3 * envConfig.pollerIntervalMilliseconds / 1000
86+
8687
const vaultBackend = new VaultBackend({
8788
vaultFactory: vaultFactory,
8889
tokenRenewThreshold: vaultTokenRenewThreshold,

lib/backends/vault-backend.js

+23-9
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,29 @@ class VaultBackend extends KVBackend {
5353
this._clients.set(clientCacheKey, client)
5454
}
5555

56+
// If we already have a cached token then inspect it...
57+
if (client.token) {
58+
try {
59+
this._logger.debug(`checking vault token expiry for role ${vaultRoleGet} on ${vaultMountPointGet}`)
60+
const tokenStatus = await client.tokenLookupSelf()
61+
this._logger.debug(`vault token (role ${vaultRoleGet} on ${vaultMountPointGet}) valid for ${tokenStatus.data.ttl} seconds, renews at ${this._tokenRenewThreshold}`)
62+
63+
// If it needs renewing, renew it.
64+
if (Number(tokenStatus.data.ttl) <= this._tokenRenewThreshold) {
65+
this._logger.debug(`renewing role ${vaultRoleGet} on ${vaultMountPointGet} vault token`)
66+
if (!(await client.tokenRenewSelf())) {
67+
this._logger.debug(`cached token renewal failed. Clearing cached token for role ${vaultRoleGet} on ${vaultMountPointGet}`)
68+
client.token = null
69+
}
70+
}
71+
} catch {
72+
// If it can't be inspected/renewed, we clear the token.
73+
this._logger.debug(`cached token operation failed. Clearing cached token for role ${vaultRoleGet} on ${vaultMountPointGet}`)
74+
client.token = null
75+
}
76+
}
77+
78+
// If we don't have a token here we either never had one or we just failed to renew it, so get a new one by logging-in
5679
if (!client.token) {
5780
const jwt = this._fetchServiceAccountToken()
5881
this._logger.debug(`fetching new token from vault for role ${vaultRoleGet} on ${vaultMountPointGet}`)
@@ -61,15 +84,6 @@ class VaultBackend extends KVBackend {
6184
role: vaultRoleGet,
6285
jwt: jwt
6386
})
64-
} else {
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}`)
68-
69-
if (Number(tokenStatus.data.ttl) <= this._tokenRenewThreshold) {
70-
this._logger.debug(`renewing role ${vaultRoleGet} on ${vaultMountPointGet} vault token`)
71-
await client.tokenRenewSelf()
72-
}
7387
}
7488

7589
this._logger.debug(`reading secret key ${key} from vault`)

lib/backends/vault-backend.test.js

+88
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('VaultBackend', () => {
4040
}
4141

4242
const vaultTokenRenewThreshold = 30
43+
4344
const mockTokenLookupResultMustRenew = {
4445
data: {
4546
ttl: 15
@@ -213,6 +214,93 @@ describe('VaultBackend', () => {
213214
expect(secretPropertyValue).equals(quotedSecretValue)
214215
})
215216

217+
it('returns secret property value after failed renew token and then relogin', async () => {
218+
clientMock.token = 'an-existing-token'
219+
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
220+
clientMock.tokenRenewSelf = sinon.stub().returns(false)
221+
222+
const secretPropertyValue = await vaultBackend._get({
223+
specOptions: {
224+
vaultMountPoint: mountPoint,
225+
vaultRole: role
226+
},
227+
key: secretKey
228+
})
229+
230+
// check the token happened ...
231+
sinon.assert.calledOnce(clientMock.tokenLookupSelf)
232+
233+
// ... then try to renew the token ...
234+
sinon.assert.calledOnce(clientMock.tokenRenewSelf)
235+
236+
// ... now relogin due to failure to renew ...
237+
sinon.assert.calledOnce(clientMock.kubernetesLogin)
238+
239+
// ... then we fetch the secret ...
240+
sinon.assert.calledWith(clientMock.read, secretKey)
241+
242+
// ... and expect to get its proper value
243+
expect(secretPropertyValue).equals(quotedSecretValue)
244+
})
245+
246+
it('returns secret property value after errored token inspection and then relogin', async () => {
247+
clientMock.token = 'an-existing-token'
248+
clientMock.tokenLookupSelf = sinon.stub().throws(new Error())
249+
clientMock.tokenRenewSelf = sinon.stub().returns(false)
250+
251+
const secretPropertyValue = await vaultBackend._get({
252+
specOptions: {
253+
vaultMountPoint: mountPoint,
254+
vaultRole: role
255+
},
256+
key: secretKey
257+
})
258+
259+
// check the token happened ...
260+
sinon.assert.calledOnce(clientMock.tokenLookupSelf)
261+
262+
// ... didn't try to renew the token ...
263+
sinon.assert.notCalled(clientMock.tokenRenewSelf)
264+
265+
// ... but instead just relogin due to error upon inspection ...
266+
sinon.assert.calledOnce(clientMock.kubernetesLogin)
267+
268+
// ... then we fetch the secret ...
269+
sinon.assert.calledWith(clientMock.read, secretKey)
270+
271+
// ... and expect to get its proper value
272+
expect(secretPropertyValue).equals(quotedSecretValue)
273+
})
274+
275+
it('returns secret property value after errored token renewal and then relogin', async () => {
276+
clientMock.token = 'an-existing-token'
277+
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
278+
clientMock.tokenRenewSelf = sinon.stub().throws(new Error())
279+
280+
const secretPropertyValue = await vaultBackend._get({
281+
specOptions: {
282+
vaultMountPoint: mountPoint,
283+
vaultRole: role
284+
},
285+
key: secretKey
286+
})
287+
288+
// check the token happened ...
289+
sinon.assert.calledOnce(clientMock.tokenLookupSelf)
290+
291+
// ... didn't try to renew the token ...
292+
sinon.assert.calledOnce(clientMock.tokenRenewSelf)
293+
294+
// ... but instead just relogin due to error upon renewal ...
295+
sinon.assert.calledOnce(clientMock.kubernetesLogin)
296+
297+
// ... then we fetch the secret ...
298+
sinon.assert.calledWith(clientMock.read, secretKey)
299+
300+
// ... and expect to get its proper value
301+
expect(secretPropertyValue).equals(quotedSecretValue)
302+
})
303+
216304
it('returns secret property value if a token exists that does not need renewal', async () => {
217305
clientMock.token = 'an-existing-token'
218306
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultNoRenew)

0 commit comments

Comments
 (0)