Skip to content

Commit 1e8c05c

Browse files
committed
Allow using NGINX keyval store for credential caching
1 parent 4e123f3 commit 1e8c05c

File tree

5 files changed

+187
-23
lines changed

5 files changed

+187
-23
lines changed

common/etc/nginx/include/s3gateway.js

Lines changed: 117 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,60 @@ function _credentialsTempFile() {
150150
}
151151

152152
/**
153-
* Read the contents of the credentials file into memory. If it is not
154-
* found, then return undefined.
153+
* Write the instance profile credentials to a caching backend.
155154
*
156-
* @returns {undefined|object} AWS instance profile credentials or undefined
155+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
156+
* @param credentials {{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials
157157
*/
158-
function readCredentials() {
158+
function writeCredentials(r, credentials) {
159+
// Do not bother writing credentials if we are running in a mode where we
160+
// do not need instance credentials.
161+
if (process.env['S3_ACCESS_KEY_ID'] && process.env['S3_SECRET_KEY']) {
162+
return;
163+
}
164+
165+
if (!credentials) {
166+
throw `Cannot write invalid credentials: ${JSON.stringify(credentials)}`;
167+
}
168+
169+
if ("variables" in r && r.variables.cache_instance_credentials_enabled == 1) {
170+
_writeCredentialsToKeyValStore(r, credentials);
171+
} else {
172+
_writeCredentialsToFile(credentials);
173+
}
174+
}
175+
176+
/**
177+
* Write the instance profile credentials to the NGINX Keyval store.
178+
*
179+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
180+
* @param credentials {{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials
181+
* @private
182+
*/
183+
function _writeCredentialsToKeyValStore(r, credentials) {
184+
r.variables.instance_credential_json = JSON.stringify(credentials);
185+
}
186+
187+
/**
188+
* Write the instance profile credentials to a file on the file system. This
189+
* file will be quite small and should end up in the file cache relatively
190+
* quickly if it is repeatedly read.
191+
*
192+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
193+
* @param credentials {{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials
194+
* @private
195+
*/
196+
function _writeCredentialsToFile(credentials) {
197+
fs.writeFileSync(_credentialsTempFile(), JSON.stringify(credentials));
198+
}
199+
200+
/**
201+
* Get the instance profile credentials needed to authenticated against S3 from
202+
* a backend cache. If the credentials cannot be found, then return undefined.
203+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
204+
* @returns {undefined|{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials or undefined
205+
*/
206+
function readCredentials(r) {
159207
if (process.env['S3_ACCESS_KEY_ID'] && process.env['S3_SECRET_KEY']) {
160208
return {
161209
accessKeyId: process.env['S3_ACCESS_KEY_ID'],
@@ -165,6 +213,44 @@ function readCredentials() {
165213
};
166214
}
167215

216+
if ("variables" in r && r.variables.cache_instance_credentials_enabled == 1) {
217+
return _readCredentialsFromKeyValStore(r);
218+
} else {
219+
return _readCredentialsFromFile();
220+
}
221+
}
222+
223+
/**
224+
* Read credentials from the NGINX Keyval store. If it is not found, then
225+
* return undefined.
226+
*
227+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
228+
* @returns {undefined|{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials or undefined
229+
* @private
230+
*/
231+
function _readCredentialsFromKeyValStore(r) {
232+
var cached = r.variables.instance_credential_json;
233+
234+
if (!cached) {
235+
return undefined;
236+
}
237+
238+
try {
239+
return JSON.parse(cached);
240+
} catch (e) {
241+
_debug_log(r, `Error parsing JSON value from r.variables.instance_credential_json: ${e}`);
242+
return undefined;
243+
}
244+
}
245+
246+
/**
247+
* Read the contents of the credentials file into memory. If it is not
248+
* found, then return undefined.
249+
*
250+
* @returns {undefined|{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}} AWS instance profile credentials or undefined
251+
* @private
252+
*/
253+
function _readCredentialsFromFile() {
168254
var credsFilePath = _credentialsTempFile();
169255

170256
try {
@@ -201,7 +287,7 @@ function s3auth(r) {
201287

202288
var signature;
203289

204-
var credentials = readCredentials();
290+
var credentials = readCredentials(r);
205291
if (sigver == '2') {
206292
signature = signatureV2(r, bucket, credentials);
207293
} else {
@@ -211,8 +297,14 @@ function s3auth(r) {
211297
return signature;
212298
}
213299

214-
function s3SecurityToken() {
215-
var credentials = readCredentials();
300+
/**
301+
* Get the current session token from the instance profile credential cache.
302+
*
303+
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
304+
* @returns {string} current session token or empty string
305+
*/
306+
function s3SecurityToken(r) {
307+
var credentials = readCredentials(r);
216308
if (credentials.sessionToken) {
217309
return credentials.sessionToken;
218310
}
@@ -782,7 +874,21 @@ var maxValidityOffsetMs = 4.5 * 60 * 100;
782874
* @returns {Promise<void>}
783875
*/
784876
async function fetchCredentials(r) {
785-
var current = readCredentials();
877+
// If we are not using an AWS instance profile to set our credentials we
878+
// exit quickly and don't write a credentials file.
879+
if (process.env['S3_ACCESS_KEY_ID'] && process.env['S3_SECRET_KEY']) {
880+
r.return(200);
881+
return;
882+
}
883+
884+
try {
885+
var current = readCredentials(r);
886+
} catch (e) {
887+
_debug_log(r, `Could not read credentials: ${e}`);
888+
r.return(500);
889+
return;
890+
}
891+
786892
if (current) {
787893
var exp = new Date(current.expiration).getTime() - maxValidityOffsetMs;
788894
if (now.getTime() < exp) {
@@ -793,13 +899,6 @@ async function fetchCredentials(r) {
793899

794900
var credentials;
795901

796-
// If we are not using an AWS instance profile to set our credentials we
797-
// exit quickly and don't write a credentials file.
798-
if (process.env['S3_ACCESS_KEY_ID'] && process.env['S3_SECRET_KEY']) {
799-
r.return(200);
800-
return;
801-
}
802-
803902
_debug_log(r, 'Cached credentials are expired or not present, requesting new ones');
804903

805904
if (process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI']) {
@@ -821,9 +920,9 @@ async function fetchCredentials(r) {
821920
}
822921
}
823922
try {
824-
fs.writeFileSync(_credentialsTempFile(), JSON.stringify(credentials));
923+
writeCredentials(r, credentials);
825924
} catch (e) {
826-
_debug_log(r, 'Could not write credentials file: ' + JSON.stringify(e));
925+
_debug_log(r, `Could not write credentials: ${e}`);
827926
r.return(500);
828927
return;
829928
}
@@ -899,6 +998,7 @@ export default {
899998
awsHeaderDate,
900999
fetchCredentials,
9011000
readCredentials,
1001+
writeCredentials,
9021002
s3date,
9031003
s3auth,
9041004
s3SecurityToken,

oss/etc/nginx/conf.d/gateway/server_variables.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
# caching is turned off. This feature uses the keyval store, so it
33
# is only enabled when using NGINX Plus.
44
set $cache_signing_key_enabled 0;
5+
6+
# Variable indicating to the s3gateway.js script that session token
7+
# caching is turned on. This feature uses the keyval store, so it
8+
# is only enabled when using NGINX Plus.
9+
set $cache_instance_credentials_enabled 0;

plus/etc/nginx/conf.d/gateway/server_variables.conf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
# caching is turned on. This feature uses the keyval store, so it
33
# is only enabled when using NGINX Plus.
44
set $cache_signing_key_enabled 1;
5+
6+
# Variable indicating to the s3gateway.js script that session token
7+
# caching is turned on. This feature uses the keyval store, so it
8+
# is only enabled when using NGINX Plus.
9+
set $cache_instance_credentials_enabled 1;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# This key value zone allows us to cache a portion of the cryptographic
2+
# signatures used by AWS v4 signatures.
3+
keyval_zone zone=instance_credential_cache:32k type=string timeout=6h;
4+
keyval 'instance_credential' $instance_credential_json zone=instance_credential_cache;

test/unit/s3gateway_test.js

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -311,11 +311,12 @@ function testEscapeURIPathPreservesDoubleSlashes() {
311311

312312
function testReadCredentialsWithAccessAndSecretKeySet() {
313313
printHeader('testReadCredentialsWithAccessAndSecretKeySet');
314+
let r = {};
314315
process.env['S3_ACCESS_KEY_ID'] = 'SOME_ACCESS_KEY';
315316
process.env['S3_SECRET_KEY'] = 'SOME_SECRET_KEY';
316317

317318
try {
318-
var credentials = s3gateway.readCredentials();
319+
var credentials = s3gateway.readCredentials(r);
319320
if (credentials.accessKeyId !== process.env['S3_ACCESS_KEY_ID']) {
320321
throw 'static credentials do not match returned value [accessKeyId]';
321322
}
@@ -335,8 +336,14 @@ function testReadCredentialsWithAccessAndSecretKeySet() {
335336
}
336337
}
337338

338-
function testReadCredentials() {
339-
printHeader('testReadCredentials');
339+
function testReadCredentialsFromFilePath() {
340+
printHeader('testReadCredentialsFromFilePath');
341+
let r = {
342+
variables: {
343+
cache_instance_credentials_enabled: 0
344+
}
345+
};
346+
340347
var originalCredentialPath = process.env['S3_CREDENTIALS_TEMP_FILE'];
341348
var tempDir = (process.env['TMPDIR'] ? process.env['TMPDIR'] : '/tmp');
342349
var uniqId = `${new Date().getTime()}-${Math.floor(Math.random()*101)}`;
@@ -347,7 +354,7 @@ function testReadCredentials() {
347354

348355
try {
349356
process.env['S3_CREDENTIALS_TEMP_FILE'] = tempFile;
350-
var credentials = s3gateway.readCredentials();
357+
var credentials = s3gateway.readCredentials(r);
351358
var testDataAsJSON = JSON.parse(testData);
352359
if (credentials.accessKeyId !== testDataAsJSON.accessKeyId) {
353360
throw 'JSON test data does not match credentials [accessKeyId]';
@@ -373,14 +380,19 @@ function testReadCredentials() {
373380

374381
function testReadCredentialsFromNonexistentPath() {
375382
printHeader('testReadCredentialsFromNonexistentPath');
383+
let r = {
384+
variables: {
385+
cache_instance_credentials_enabled: 0
386+
}
387+
};
376388
var originalCredentialPath = process.env['S3_CREDENTIALS_TEMP_FILE'];
377389
var tempDir = (process.env['TMPDIR'] ? process.env['TMPDIR'] : '/tmp');
378390
var uniqId = `${new Date().getTime()}-${Math.floor(Math.random()*101)}`;
379391
var tempFile = `${tempDir}/credentials-unit-test-${uniqId}.json`;
380392

381393
try {
382394
process.env['S3_CREDENTIALS_TEMP_FILE'] = tempFile;
383-
var credentials = s3gateway.readCredentials();
395+
var credentials = s3gateway.readCredentials(r);
384396
if (credentials !== undefined) {
385397
throw 'Credentials returned when no credentials file should be present';
386398
}
@@ -395,6 +407,43 @@ function testReadCredentialsFromNonexistentPath() {
395407
}
396408
}
397409

410+
function testReadAndWriteCredentialsFromKeyValStore() {
411+
printHeader('testReadAndWriteCredentialsFromKeyValStore');
412+
413+
let accessKeyId = process.env['S3_ACCESS_KEY_ID'];
414+
let secretKey = process.env['S3_SECRET_KEY'];
415+
delete process.env.S3_ACCESS_KEY_ID;
416+
delete process.env.S3_SECRET_KEY;
417+
418+
try {
419+
let r = {
420+
variables: {
421+
cache_instance_credentials_enabled: 1,
422+
instance_credential_json: null
423+
}
424+
};
425+
let expectedCredentials = {
426+
AccessKeyId: 'AN_ACCESS_KEY_ID',
427+
Expiration: '2017-05-17T15:09:54Z',
428+
RoleArn: 'TASK_ROLE_ARN',
429+
SecretAccessKey: 'A_SECRET_ACCESS_KEY',
430+
Token: 'A_SECURITY_TOKEN',
431+
};
432+
433+
s3gateway.writeCredentials(r, expectedCredentials);
434+
let credentials = JSON.stringify(s3gateway.readCredentials(r));
435+
let expectedJson = JSON.stringify(expectedCredentials);
436+
437+
if (credentials !== expectedJson) {
438+
console.log(`EXPECTED:\n${expectedJson}\nACTUAL:\n${credentials}`);
439+
throw 'Credentials do not match expected value';
440+
}
441+
} finally {
442+
process.env['S3_ACCESS_KEY_ID'] = accessKeyId;
443+
process.env['S3_SECRET_KEY'] = secretKey;
444+
}
445+
}
446+
398447
async function testEcsCredentialRetrieval() {
399448
printHeader('testEcsCredentialRetrieval');
400449
process.env['S3_ACCESS_KEY_ID'] = undefined;
@@ -531,8 +580,9 @@ async function test() {
531580
testEditAmzHeadersHeadDirectory();
532581
testEscapeURIPathPreservesDoubleSlashes();
533582
testReadCredentialsWithAccessAndSecretKeySet();
534-
testReadCredentials();
583+
testReadCredentialsFromFilePath();
535584
testReadCredentialsFromNonexistentPath();
585+
testReadAndWriteCredentialsFromKeyValStore();
536586
await testEcsCredentialRetrieval();
537587
await testEc2CredentialRetrieval();
538588
}

0 commit comments

Comments
 (0)