diff --git a/common/etc/nginx/include/awscredentials.js b/common/etc/nginx/include/awscredentials.js index 417a04d8..c473e0da 100644 --- a/common/etc/nginx/include/awscredentials.js +++ b/common/etc/nginx/include/awscredentials.js @@ -41,7 +41,7 @@ function sessionToken(r) { */ function readCredentials(r) { // TODO: Change the generic constants naming for multiple AWS services. - if ('S3_ACCESS_KEY_ID' in process.env && 'S3_SECRET_KEY' in process.env) { + if ('S3_ACCESS_KEY_ID' in process.env && 'S3_SECRET_KEY' in process.env && !('AWS_ROLE_ARN' in process.env)) { const sessionToken = 'S3_SESSION_TOKEN' in process.env ? process.env['S3_SESSION_TOKEN'] : null; return { @@ -132,7 +132,7 @@ function _credentialsTempFile() { function writeCredentials(r, credentials) { /* Do not bother writing credentials if we are running in a mode where we do not need instance credentials. */ - if (process.env['S3_ACCESS_KEY_ID'] && process.env['S3_SECRET_KEY']) { + if (process.env['S3_ACCESS_KEY_ID'] && process.env['S3_SECRET_KEY'] && !('AWS_ROLE_ARN' in process.env)) { return; } diff --git a/common/etc/nginx/include/awssig2.js b/common/etc/nginx/include/awssig2.js index 5630b212..8997e9c8 100644 --- a/common/etc/nginx/include/awssig2.js +++ b/common/etc/nginx/include/awssig2.js @@ -22,14 +22,14 @@ const mod_hmac = require('crypto'); * Create HTTP Authorization header for authenticating with an AWS compatible * v2 API. * - * @param r {Request} HTTP request object + * @param r {Request} HTTP request object (for logging only) + * @param method {string} The http method * @param uri {string} The URI-encoded version of the absolute path component URL to create a request * @param httpDate {string} RFC2616 timestamp used to sign the request * @param credentials {object} Credential object with AWS credentials in it (AccessKeyId, SecretAccessKey, SessionToken) * @returns {string} HTTP Authorization header value */ -function signatureV2(r, uri, httpDate, credentials) { - const method = r.method; +function signatureV2(r, method, uri, httpDate, credentials) { const hmac = mod_hmac.createHmac('sha1', credentials.secretAccessKey); const stringToSign = method + '\n\n\n' + httpDate + '\n' + uri; diff --git a/common/etc/nginx/include/awssig4.js b/common/etc/nginx/include/awssig4.js index 6206796c..4e1b9a50 100644 --- a/common/etc/nginx/include/awssig4.js +++ b/common/etc/nginx/include/awssig4.js @@ -35,21 +35,22 @@ const DEFAULT_SIGNED_HEADERS = 'host;x-amz-content-sha256;x-amz-date'; * Create HTTP Authorization header for authenticating with an AWS compatible * v4 API. * - * @param r {Request} HTTP request object + * @param r {Request} HTTP request object (for logging only) * @param timestamp {Date} timestamp associated with request (must fall within a skew) * @param region {string} API region associated with request * @param service {string} service code (for example, s3, lambda) - * @param uri {string} The URI-encoded version of the absolute path component URL to create a canonical request + * @param method {string} The method + * @param path {string} The URI-encoded version of the absolute path component of the URI to create a canonical request * @param queryParams {string} The URL-encoded query string parameters to create a canonical request * @param host {string} HTTP host header value * @param credentials {object} Credential object with AWS credentials in it (AccessKeyId, SecretAccessKey, SessionToken) * @returns {string} HTTP Authorization header value */ -function signatureV4(r, timestamp, region, service, uri, queryParams, host, credentials) { +function signatureV4(r, timestamp, region, service, method, path, queryParams, host, credentials) { const eightDigitDate = utils.getEightDigitDate(timestamp); const amzDatetime = utils.getAmzDatetime(timestamp, eightDigitDate); const canonicalRequest = _buildCanonicalRequest( - r.method, uri, queryParams, host, amzDatetime, credentials.sessionToken); + method, path, queryParams, host, amzDatetime, credentials.sessionToken); const signature = _buildSignatureV4(r, amzDatetime, eightDigitDate, credentials, region, service, canonicalRequest); const authHeader = 'AWS4-HMAC-SHA256 Credential=' @@ -66,14 +67,14 @@ function signatureV4(r, timestamp, region, service, uri, queryParams, host, cred * * @see {@link https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html | Creating a Canonical Request} * @param method {string} HTTP method - * @param uri {string} URI associated with request + * @param path {string} The URI-encoded version of the absolute path component of the URI * @param queryParams {string} query parameters associated with request * @param host {string} HTTP Host header value * @param amzDatetime {string} ISO8601 timestamp string to sign request with * @returns {string} string with concatenated request parameters * @private */ -function _buildCanonicalRequest(method, uri, queryParams, host, amzDatetime, sessionToken) { +function _buildCanonicalRequest(method, path, queryParams, host, amzDatetime, sessionToken) { let canonicalHeaders = 'host:' + host + '\n' + 'x-amz-content-sha256:' + EMPTY_PAYLOAD_HASH + '\n' + 'x-amz-date:' + amzDatetime + '\n'; @@ -83,7 +84,7 @@ function _buildCanonicalRequest(method, uri, queryParams, host, amzDatetime, ses } let canonicalRequest = method + '\n'; - canonicalRequest += uri + '\n'; + canonicalRequest += path + '\n'; canonicalRequest += queryParams + '\n'; canonicalRequest += canonicalHeaders + '\n'; canonicalRequest += _signedHeaders(sessionToken) + '\n'; @@ -253,6 +254,7 @@ function _splitCachedValues(cached) { export default { signatureV4, + EMPTY_PAYLOAD_HASH, // These functions do not need to be exposed, but they are exposed so that // unit tests can run against them. _buildCanonicalRequest, diff --git a/common/etc/nginx/include/s3gateway.js b/common/etc/nginx/include/s3gateway.js index 6d694749..ea193d13 100644 --- a/common/etc/nginx/include/s3gateway.js +++ b/common/etc/nginx/include/s3gateway.js @@ -177,12 +177,12 @@ function s3auth(r) { const credentials = awscred.readCredentials(r); if (sigver == '2') { - let req = _s3ReqParamsForSigV2(r, bucket); - signature = awssig2.signatureV2(r, req.uri, req.httpDate, credentials); + const req = _s3ReqParamsForSigV2(r, bucket); + signature = awssig2.signatureV2(r, r.method, req.path, req.httpDate, credentials); } else { - let req = _s3ReqParamsForSigV4(r, bucket, server); + const req = _s3ReqParamsForSigV4(r, bucket, server); signature = awssig4.signatureV4(r, NOW, region, SERVICE, - req.uri, req.queryParams, req.host, credentials); + r.method, req.uri, req.queryParams, req.host, credentials); } return signature; @@ -194,7 +194,7 @@ function s3auth(r) { * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/userguide/auth-request-sig-v2.html | AWS signature version 2} * @param r {Request} HTTP request object * @param bucket {string} S3 bucket associated with request - * @returns s3ReqParams {object} s3ReqParams object (host, method, uri, queryParams) + * @returns s3ReqParams {object} s3ReqParams object (host, method, path, queryParams) * @private */ function _s3ReqParamsForSigV2(r, bucket) { @@ -203,14 +203,14 @@ function _s3ReqParamsForSigV2(r, bucket) { * string to sign. For example, if we are requesting /bucket/dir1/ from * nginx, then in S3 we need to request /?delimiter=/&prefix=dir1/ * Thus, we can't put the path /dir1/ in the string to sign. */ - let uri = _isDirectory(r.variables.uri_path) ? '/' : r.variables.uri_path; + let path = _isDirectory(r.variables.uri_path) ? '/' : r.variables.uri_path; // To return index pages + index.html if (PROVIDE_INDEX_PAGE && _isDirectory(r.variables.uri_path)){ - uri = r.variables.uri_path + INDEX_PAGE + path = r.variables.uri_path + INDEX_PAGE } return { - uri: '/' + bucket + uri, + path: '/' + bucket + path, httpDate: s3date(r) }; } @@ -222,7 +222,7 @@ function _s3ReqParamsForSigV2(r, bucket) { * @param r {Request} HTTP request object * @param bucket {string} S3 bucket associated with request * @param server {string} S3 host associated with request - * @returns s3ReqParams {object} s3ReqParams object (host, uri, queryParams) + * @returns s3ReqParams {object} s3ReqParams object (host, path, queryParams) * @private */ function _s3ReqParamsForSigV4(r, bucket, server) { @@ -232,19 +232,19 @@ function _s3ReqParamsForSigV4(r, bucket, server) { } const baseUri = s3BaseUri(r); const queryParams = _s3DirQueryParams(r.variables.uri_path, r.method); - let uri; + let path; if (queryParams.length > 0) { if (baseUri.length > 0) { - uri = baseUri; + path = baseUri; } else { - uri = '/'; + path = '/'; } } else { - uri = s3uri(r); + path = s3uri(r); } return { host: host, - uri: uri, + path: path, queryParams: queryParams }; } @@ -509,7 +509,7 @@ const maxValidityOffsetMs = 4.5 * 60 * 1000; async function fetchCredentials(r) { /* If we are not using an AWS instance profile to set our credentials we exit quickly and don't write a credentials file. */ - if (utils.areAllEnvVarsSet(['S3_ACCESS_KEY_ID', 'S3_SECRET_KEY'])) { + if (utils.areAllEnvVarsSet('S3_ACCESS_KEY_ID', 'S3_SECRET_KEY') && !utils.areAllEnvVarsSet('AWS_ROLE_ARN')) { r.return(200); return; } @@ -558,6 +558,15 @@ async function fetchCredentials(r) { r.return(500); return; } + } + else if(utils.areAllEnvVarsSet('AWS_ROLE_ARN')) { + try { + credentials = await _fetchAssumeRoleCredentials(r); + } catch(e) { + utils.debug_log(r, `Could not assume role ${process.env['AWS_ROLE_ARN']}: ` + JSON.stringify(e)); + r.return(500); + return; + } } else { try { credentials = await _fetchEC2RoleCredentials(); @@ -643,17 +652,7 @@ async function _fetchEC2RoleCredentials() { }; } -/** - * Get the credentials by assuming calling AssumeRoleWithWebIdentity with the environment variable - * values ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE and HOSTNAME - * - * @returns {Promise<{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}>} - * @private - */ -async function _fetchWebIdentityCredentials(r) { - const arn = process.env['AWS_ROLE_ARN']; - const name = process.env['HOSTNAME'] || 'nginx-s3-gateway'; - +function _getStsEndpoint() { let sts_endpoint = process.env['STS_ENDPOINT']; if (!sts_endpoint) { /* On EKS, the ServiceAccount can be annotated with @@ -679,6 +678,20 @@ async function _fetchWebIdentityCredentials(r) { sts_endpoint = 'https://sts.amazonaws.com'; } } + return sts_endpoint; +} + +/** + * Get the credentials by assuming calling AssumeRoleWithWebIdentity with the environment variable + * values ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE and HOSTNAME + * + * @returns {Promise<{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}>} + * @private + */ +async function _fetchWebIdentityCredentials(r) { + const arn = process.env['AWS_ROLE_ARN']; + const name = process.env['HOSTNAME'] || 'nginx-s3-gateway'; + const sts_endpoint = _getStsEndpoint(); const token = fs.readFileSync(process.env['AWS_WEB_IDENTITY_TOKEN_FILE']); @@ -702,6 +715,48 @@ async function _fetchWebIdentityCredentials(r) { }; } +/** + * Get the credentials by assuming calling AssumeRole with the environment variable + * values AWS_ROLE_ARN, SECRET_ACCESS_KEY and ACCESS_KEY_ID + * + * @returns {Promise<{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}>} + * @private + */ +async function _fetchAssumeRoleCredentials(r) { + const tempCreds = { + accessKeyId: process.env['S3_ACCESS_KEY_ID'], + secretAccessKey: process.env['S3_SECRET_KEY'], + }; + const arn = process.env['AWS_ROLE_ARN']; + const name = process.env['HOSTNAME'] || 'nginx-s3-gateway'; + const params = `Action=AssumeRole&RoleArn=${encodeURIComponent(arn)}&RoleSessionName=${encodeURIComponent(name)}&Version=2011-06-15`; + const sts_endpoint = _getStsEndpoint(); + const host = sts_endpoint.slice(8); + const method = 'GET'; + const region = process.env['AWS_REGION']; + const signature = awssig4.signatureV4(r, NOW, region, 'sts', method, '/', params, host, tempCreds); + const url = sts_endpoint + "?" + params; + const response = await ngx.fetch(url, { + headers: { + "Authorization": signature, + "X-Amz-Date": amzDatetime, + 'X-Amz-Content-Sha256': awssig4.EMPTY_PAYLOAD_HASH, + "Accept": "application/json" + }, + method: method, + }); + + const resp = await response.json(); + const creds = resp.AssumeRoleResponse.AssumeRoleResult.Credentials; + + return { + accessKeyId: creds.AccessKeyId, + secretAccessKey: creds.SecretAccessKey, + sessionToken: creds.SessionToken, + expiration: creds.Expiration, + }; +} + export default { awsHeaderDate, fetchCredentials, diff --git a/test/unit/awssig2_test.js b/test/unit/awssig2_test.js index 67b69080..ee900509 100644 --- a/test/unit/awssig2_test.js +++ b/test/unit/awssig2_test.js @@ -37,7 +37,7 @@ function _runSignatureV2(r) { const httpDate = timestamp.toUTCString(); const expected = 'AWS test-access-key-1:VviSS4cFhUC6eoB4CYqtRawzDrc='; let req = s3gateway._s3ReqParamsForSigV2(r, bucket); - let signature = awssig2.signatureV2(r, req.uri, httpDate, creds); + let signature = awssig2.signatureV2(r, 'GET', req.path, httpDate, creds); if (signature !== expected) { throw 'V2 signature hash was not created correctly.\n' + diff --git a/test/unit/awssig4_test.js b/test/unit/awssig4_test.js index 0c1677a1..7424cc37 100644 --- a/test/unit/awssig4_test.js +++ b/test/unit/awssig4_test.js @@ -71,7 +71,7 @@ function _runSignatureV4(r) { // awssig4.js for the purpose of common library. let req = s3gateway._s3ReqParamsForSigV4(r, bucket, server); const canonicalRequest = awssig4._buildCanonicalRequest( - r.method, req.uri, req.queryParams, req.host, amzDatetime, creds.sessionToken); + r.method, req.path, req.queryParams, req.host, amzDatetime, creds.sessionToken); var expected = 'cf4dd9e1d28c74e2284f938011efc8230d0c20704f56f67e4a3bfc2212026bec'; var signature = awssig4._buildSignatureV4(