Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for assuming the role specified in AWS_ROLE_ARN when not using WebIdentity #121

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions common/etc/nginx/include/awscredentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down
6 changes: 3 additions & 3 deletions common/etc/nginx/include/awssig2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
16 changes: 9 additions & 7 deletions common/etc/nginx/include/awssig4.js
Original file line number Diff line number Diff line change
Expand Up @@ -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='
Expand All @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
107 changes: 81 additions & 26 deletions common/etc/nginx/include/s3gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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)
};
}
Expand All @@ -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) {
Expand All @@ -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
};
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand All @@ -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']);

Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion test/unit/awssig2_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' +
Expand Down
2 changes: 1 addition & 1 deletion test/unit/awssig4_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down