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

Commit a52987b

Browse files
authored
fix(vault): token ttl conditional renew (#457)
* fix(vault-backend): token ttl conditional renew
1 parent 6fd9a42 commit a52987b

File tree

5 files changed

+71
-8
lines changed

5 files changed

+71
-8
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,8 @@ spec:
352352
353353
If you use Vault Namespaces (a Vault Enterprise feature) you can set the namespace to interact with via the `VAULT_NAMESPACE` environment variable.
354354

355+
The Vault token obtained by Kubernetes authentication will be renewed as needed. By default the token will be renewed three poller intervals (POLLER_INTERVAL_MILLISECONDS) before the token TTL expires. The default should be acceptable in most cases but the token renew threshold can also be customized by setting the `VAULT_TOKEN_RENEW_THRESHOLD` environment variable. The token renew threshold value is specified in seconds and tokens with remaining TTL less than this number of seconds will be renewed. In order to minimize token renewal load on the Vault server it is suggested that Kubernetes auth tokens issued by Vault have a TTL of at least ten times the poller interval so that they are renewed less frequently. A longer token TTL results in a lower token renewal load on Vault.
356+
355357
If Vault uses a certificate issued by a self-signed CA you will need to provide that certificate:
356358

357359
```sh

config/environment.js

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ if (environment === 'development') {
1919
const vaultEndpoint = process.env.VAULT_ADDR || 'http://127.0.0.1:8200'
2020
// Grab the vault namespace from the environment
2121
const vaultNamespace = process.env.VAULT_NAMESPACE || null
22+
const vaultTokenRenewThreshold = process.env.VAULT_TOKEN_RENEW_THRESHOLD || null
2223

2324
const pollerIntervalMilliseconds = process.env.POLLER_INTERVAL_MILLISECONDS
2425
? Number(process.env.POLLER_INTERVAL_MILLISECONDS) : 10000
@@ -40,6 +41,7 @@ const customResourceManagerDisabled = 'DISABLE_CUSTOM_RESOURCE_MANAGER' in proce
4041
module.exports = {
4142
vaultEndpoint,
4243
vaultNamespace,
44+
vaultTokenRenewThreshold,
4345
environment,
4446
pollerIntervalMilliseconds,
4547
metricsPort,

config/index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ if (envConfig.vaultNamespace) {
7777
}
7878
}
7979
const vaultClient = vault(vaultOptions)
80-
const vaultBackend = new VaultBackend({ client: vaultClient, logger })
80+
// The Vault token is renewed only during polling, not asynchronously. The default tokenRenewThreshold
81+
// is three times larger than the pollerInterval so that the token is renewed before it
82+
// expires and with at least one remaining poll opportunty to retry renewal if it fails.
83+
const vaultTokenRenewThreshold = envConfig.vaultTokenRenewThreshold
84+
? Number(envConfig.vaultTokenRenewThreshold) : 3 * envConfig.pollerIntervalMilliseconds / 1000
85+
const vaultBackend = new VaultBackend({ client: vaultClient, tokenRenewThreshold: vaultTokenRenewThreshold, logger })
8186
const azureKeyVaultBackend = new AzureKeyVaultBackend({
8287
credential: azureConfig.azureKeyVault(),
8388
logger

lib/backends/vault-backend.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ class VaultBackend extends KVBackend {
77
/**
88
* Create Vault backend.
99
* @param {Object} client - Client for interacting with Vault.
10+
* @param {Number} tokenRenewThreshold - tokens are renewed when ttl reaches this threshold
1011
* @param {Object} logger - Logger for logging stuff.
1112
*/
12-
constructor ({ client, logger }) {
13+
constructor ({ client, tokenRenewThreshold, logger }) {
1314
super({ logger })
1415
this._client = client
16+
this._tokenRenewThreshold = tokenRenewThreshold
1517
}
1618

1719
/**
@@ -40,15 +42,20 @@ class VaultBackend extends KVBackend {
4042
if (!this._client.token) {
4143
const jwt = this._fetchServiceAccountToken()
4244
this._logger.debug('fetching new token from vault')
43-
const vault = await this._client.kubernetesLogin({
45+
await this._client.kubernetesLogin({
4446
mount_point: vaultMountPoint,
4547
role: vaultRole,
4648
jwt: jwt
4749
})
48-
this._client.token = vault.auth.client_token
4950
} else {
50-
this._logger.debug('renewing existing token from vault')
51-
this._client.tokenRenewSelf()
51+
this._logger.debug('checking vault token expiry')
52+
const tokenStatus = await this._client.tokenLookupSelf()
53+
this._logger.debug(`vault token valid for ${tokenStatus.data.ttl} seconds, renews at ${this._tokenRenewThreshold}`)
54+
55+
if (Number(tokenStatus.data.ttl) <= this._tokenRenewThreshold) {
56+
this._logger.debug('renewing vault token')
57+
await this._client.tokenRenewSelf()
58+
}
5259
}
5360

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

lib/backends/vault-backend.test.js

+49-2
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,32 @@ describe('VaultBackend', () => {
3434
}
3535
}
3636

37+
const vaultTokenRenewThreshold = 30
38+
const mockTokenLookupResultMustRenew = {
39+
data: {
40+
ttl: 15
41+
}
42+
}
43+
const mockTokenLookupResultNoRenew = {
44+
data: {
45+
ttl: 60
46+
}
47+
}
48+
3749
beforeEach(() => {
3850
clientMock = sinon.mock()
3951

4052
vaultBackend = new VaultBackend({
4153
client: clientMock,
54+
tokenRenewThreshold: vaultTokenRenewThreshold,
4255
logger
4356
})
4457
})
4558

4659
describe('_get', () => {
4760
beforeEach(() => {
4861
clientMock.read = sinon.stub().returns(kv2Secret)
62+
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
4963
clientMock.tokenRenewSelf = sinon.stub().returns(true)
5064
clientMock.kubernetesLogin = sinon.stub().returns({
5165
auth: {
@@ -133,8 +147,9 @@ describe('VaultBackend', () => {
133147
expect(secretPropertyValue).equals(quotedSecretValue)
134148
})
135149

136-
it('returns secret property value after renewing token if a token exists', async () => {
150+
it('returns secret property value after renewing token if a token exists that needs renewal', async () => {
137151
clientMock.token = 'an-existing-token'
152+
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
138153

139154
const secretPropertyValue = await vaultBackend._get({
140155
specOptions: {
@@ -147,7 +162,10 @@ describe('VaultBackend', () => {
147162
// No logging into Vault...
148163
sinon.assert.notCalled(clientMock.kubernetesLogin)
149164

150-
// ... but renew the token instead ...
165+
// ... but check the token instead ...
166+
sinon.assert.calledOnce(clientMock.tokenLookupSelf)
167+
168+
// ... then renew the token ...
151169
sinon.assert.calledOnce(clientMock.tokenRenewSelf)
152170

153171
// ... then we fetch the secret ...
@@ -156,11 +174,40 @@ describe('VaultBackend', () => {
156174
// ... and expect to get its proper value
157175
expect(secretPropertyValue).equals(quotedSecretValue)
158176
})
177+
178+
it('returns secret property value if a token exists that does not need renewal', async () => {
179+
clientMock.token = 'an-existing-token'
180+
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultNoRenew)
181+
182+
const secretPropertyValue = await vaultBackend._get({
183+
specOptions: {
184+
vaultMountPoint: mountPoint,
185+
vaultRole: role
186+
},
187+
key: secretKey
188+
})
189+
190+
// No logging into Vault...
191+
sinon.assert.notCalled(clientMock.kubernetesLogin)
192+
193+
// ... but check the token instead ...
194+
sinon.assert.calledOnce(clientMock.tokenLookupSelf)
195+
196+
// ... and token does not need renewal ...
197+
sinon.assert.notCalled(clientMock.tokenRenewSelf)
198+
199+
// ... then we fetch the secret ...
200+
sinon.assert.calledWith(clientMock.read, secretKey)
201+
202+
// ... and expect to get its proper value
203+
expect(secretPropertyValue).equals(quotedSecretValue)
204+
})
159205
})
160206

161207
describe('getSecretManifestData', () => {
162208
beforeEach(() => {
163209
clientMock.read = sinon.stub().returns(kv2Secret)
210+
clientMock.tokenLookupSelf = sinon.stub().returns(mockTokenLookupResultMustRenew)
164211
clientMock.tokenRenewSelf = sinon.stub().returns(true)
165212
clientMock.kubernetesLogin = sinon.stub().returns({ auth: { client_token: '1234' } })
166213

0 commit comments

Comments
 (0)