Skip to content

Add payload hash func and default host var; Move fetching credentials func to sig lib #126

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

Merged
merged 4 commits into from
May 17, 2023
Merged
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
29 changes: 27 additions & 2 deletions common/docker-entrypoint.d/00-check-for-required-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ if [[ -v AWS_CONTAINER_CREDENTIALS_RELATIVE_URI ]]; then
echo "Running inside an ECS task, using container credentials"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a section to this script that detects the presence of S3_ environment variables, emits warnings that they are deprecated, and assigns those environment variables to the appropriate AWS_ environment variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a great point. Thanks! Addressed. Let me know if you have any additional comments.

elif [[ -v S3_SESSION_TOKEN ]]; then
echo "Depreciated the S3_SESSION_TOKEN! Use the environment variable of AWS_SESSION_TOKEN instead"
failed=1

elif [[ -v AWS_SESSION_TOKEN ]]; then
echo "S3 Session token specified - not using IMDS for credentials"

# b) Using Instance Metadata Service (IMDS) credentials, if IMDS is present at http://169.254.169.254.
Expand All @@ -48,11 +52,32 @@ elif curl --output /dev/null --silent --head --fail --connect-timeout 2 --max-ti
# Example: We are running inside an EKS cluster with IAM roles for service accounts enabled.
elif [[ -v AWS_WEB_IDENTITY_TOKEN_FILE ]]; then
echo "Running inside EKS with IAM roles for service accounts"
if [[ -v HOSTNAME ]]; then
echo "Depreciated the HOSTNAME! Use the environment variable of AWS_ROLE_SESSION_NAME instead"
failed=1
fi
if [[ ! -v AWS_ROLE_SESSION_NAME ]]; then
# The default value is set as a nginx-s3-gateway unless the value is defined.
AWS_ROLE_SESSION_NAME="nginx-s3-gateway"
fi

elif [[ -v S3_ACCESS_KEY_ID ]]; then
echo "Depreciated the S3_ACCESS_KEY_ID! Use the environment variable of AWS_ACCESS_KEY_ID instead"
failed=1

elif [[ -v S3_SECRET_KEY ]]; then
echo "Depreciated the S3_SECRET_KEY! Use the environment variable of AWS_SECRET_ACCESS_KEY instead"
failed=1

# If none of the options above is used, require static credentials.
# See https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html.
else
required+=("S3_ACCESS_KEY_ID" "S3_SECRET_KEY")
required+=("AWS_ACCESS_KEY_ID" "AWS_SECRET_ACCESS_KEY")
fi

if [[ -v S3_DEBUG ]]; then
echo "Depreciated the S3_DEBUG! Use the environment variable of DEBUG instead"
failed=1
fi

for name in ${required[@]}; do
Expand Down Expand Up @@ -101,7 +126,7 @@ if [ $failed -gt 0 ]; then
fi

echo "S3 Backend Environment"
echo "Access Key ID: ${S3_ACCESS_KEY_ID}"
echo "Access Key ID: ${AWS_ACCESS_KEY_ID}"
echo "Origin: ${S3_SERVER_PROTO}://${S3_BUCKET_NAME}.${S3_SERVER}:${S3_SERVER_PORT}"
echo "Region: ${S3_REGION}"
echo "Addressing Style: ${S3_STYLE}"
Expand Down
282 changes: 271 additions & 11 deletions common/etc/nginx/include/awscredentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,40 @@ import utils from "./utils.js";

const fs = require('fs');

/**
* The current moment as a timestamp. This timestamp will be used across
* functions in order for there to be no variations in signatures.
* @type {Date}
*/
const NOW = new Date();

/**
* Constant base URI to fetch credentials together with the credentials relative URI, see
* https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html for more details.
* @type {string}
*/
const ECS_CREDENTIAL_BASE_URI = 'http://169.254.170.2';

/**
* @type {string}
*/
const EC2_IMDS_TOKEN_ENDPOINT = 'http://169.254.169.254/latest/api/token';

const EC2_IMDS_SECURITY_CREDENTIALS_ENDPOINT = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/';

/**
* Offset to the expiration of credentials, when they should be considered expired and refreshed. The maximum
* time here can be 5 minutes, the IMDS and ECS credentials endpoint will make sure that each returned set of credentials
* is valid for at least another 5 minutes.
*
* To make sure we always refresh the credentials instead of retrieving the same again, keep credentials until 4:30 minutes
* before they really expire.
*
* @type {number}
*/
const maxValidityOffsetMs = 4.5 * 60 * 1000;


/**
* Get the current session token from either the instance profile credential
* cache or environment variables.
Expand All @@ -34,24 +68,25 @@ function sessionToken(r) {
}

/**
* Get the instance profile credentials needed to authenticated against S3 from
* Get the instance profile credentials needed to authenticate against S3 from
* a backend cache. If the credentials cannot be found, then return undefined.
* @param r {Request} HTTP request object (not used, but required for NGINX configuration)
* @returns {undefined|{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string|null), expiration: (string|null)}} AWS instance profile credentials or undefined
*/
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) {
const sessionToken = 'S3_SESSION_TOKEN' in process.env ?
process.env['S3_SESSION_TOKEN'] : null;
if ('AWS_ACCESS_KEY_ID' in process.env && 'AWS_SECRET_ACCESS_KEY' in process.env) {
let sessionToken = 'AWS_SESSION_TOKEN' in process.env ?
process.env['AWS_SESSION_TOKEN'] : null;
if (sessionToken !== null && sessionToken.length === 0) {
sessionToken = null;
}
return {
accessKeyId: process.env['S3_ACCESS_KEY_ID'],
secretAccessKey: process.env['S3_SECRET_KEY'],
accessKeyId: process.env['AWS_ACCESS_KEY_ID'],
secretAccessKey: process.env['AWS_SECRET_ACCESS_KEY'],
sessionToken: sessionToken,
expiration: null
};
}

if ("variables" in r && r.variables.cache_instance_credentials_enabled == 1) {
return _readCredentialsFromKeyValStore(r);
} else {
Expand Down Expand Up @@ -113,8 +148,8 @@ function _readCredentialsFromFile() {
* @private
*/
function _credentialsTempFile() {
if (process.env['S3_CREDENTIALS_TEMP_FILE']) {
return process.env['S3_CREDENTIALS_TEMP_FILE'];
if (process.env['AWS_CREDENTIALS_TEMP_FILE']) {
return process.env['AWS_CREDENTIALS_TEMP_FILE'];
}
if (process.env['TMPDIR']) {
return `${process.env['TMPDIR']}/credentials.json`
Expand All @@ -132,7 +167,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['AWS_ACCESS_KEY_ID'] && process.env['AWS_SECRET_ACCESS_KEY']) {
return;
}

Expand Down Expand Up @@ -171,7 +206,232 @@ function _writeCredentialsToFile(credentials) {
fs.writeFileSync(_credentialsTempFile(), JSON.stringify(credentials));
}

/**
* Get the credentials needed to create AWS signatures in order to authenticate
* to AWS service. If the gateway is being provided credentials via a instance
* profile credential as provided over the metadata endpoint, this function will:
* 1. Try to read the credentials from cache
* 2. Determine if the credentials are stale
* 3. If the cached credentials are missing or stale, it gets new credentials
* from the metadata endpoint.
* 4. If new credentials were pulled, it writes the credentials back to the
* cache.
*
* If the gateway is not using instance profile credentials, then this function
* quickly exits.
*
* @param r {Request} HTTP request object
* @returns {Promise<void>}
*/
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(['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'])) {
r.return(200);
return;
}

let current;

try {
current = readCredentials(r);
} catch (e) {
utils.debug_log(r, `Could not read credentials: ${e}`);
r.return(500);
return;
}

if (current) {
// If AWS returns a Unix timestamp it will be in seconds, but in Date constructor we should provide timestamp in milliseconds
// In some situations (including EC2 and Fargate) current.expiration will be an RFC 3339 string - see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials
const expireAt = typeof current.expiration == 'number' ? current.expiration * 1000 : current.expiration
const exp = new Date(expireAt).getTime() - maxValidityOffsetMs;
if (NOW.getTime() < exp) {
r.return(200);
return;
}
}

let credentials;

utils.debug_log(r, 'Cached credentials are expired or not present, requesting new ones');

if (utils.areAllEnvVarsSet('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI')) {
const relative_uri = process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] || '';
const uri = ECS_CREDENTIAL_BASE_URI + relative_uri;
try {
credentials = await _fetchEcsRoleCredentials(uri);
} catch (e) {
utils.debug_log(r, 'Could not load ECS task role credentials: ' + JSON.stringify(e));
r.return(500);
return;
}
}
else if (utils.areAllEnvVarsSet('AWS_WEB_IDENTITY_TOKEN_FILE')) {
try {
credentials = await _fetchWebIdentityCredentials(r)
} catch (e) {
utils.debug_log(r, 'Could not assume role using web identity: ' + JSON.stringify(e));
r.return(500);
return;
}
} else {
try {
credentials = await _fetchEC2RoleCredentials();
} catch (e) {
utils.debug_log(r, 'Could not load EC2 task role credentials: ' + JSON.stringify(e));
r.return(500);
return;
}
}
try {
writeCredentials(r, credentials);
} catch (e) {
utils.debug_log(r, `Could not write credentials: ${e}`);
r.return(500);
return;
}
r.return(200);
}

/**
* Get the credentials needed to generate AWS signatures from the ECS
* (Elastic Container Service) metadata endpoint.
*
* @param credentialsUri {string} endpoint to get credentials from
* @returns {Promise<{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}>}
* @private
*/
async function _fetchEcsRoleCredentials(credentialsUri) {
const resp = await ngx.fetch(credentialsUri);
if (!resp.ok) {
throw 'Credentials endpoint response was not ok.';
}
const creds = await resp.json();

return {
accessKeyId: creds.AccessKeyId,
secretAccessKey: creds.SecretAccessKey,
sessionToken: creds.Token,
expiration: creds.Expiration,
};
}

/**
* Get the credentials needed to generate AWS signatures from the EC2
* metadata endpoint.
*
* @returns {Promise<{accessKeyId: (string), secretAccessKey: (string), sessionToken: (string), expiration: (string)}>}
* @private
*/
async function _fetchEC2RoleCredentials() {
const tokenResp = await ngx.fetch(EC2_IMDS_TOKEN_ENDPOINT, {
headers: {
'x-aws-ec2-metadata-token-ttl-seconds': '21600',
},
method: 'PUT',
});
const token = await tokenResp.text();
let resp = await ngx.fetch(EC2_IMDS_SECURITY_CREDENTIALS_ENDPOINT, {
headers: {
'x-aws-ec2-metadata-token': token,
},
});
/* This _might_ get multiple possible roles in other scenarios, however,
EC2 supports attaching one role only.It should therefore be safe to take
the whole output, even given IMDS _might_ (?) be able to return multiple
roles. */
const credName = await resp.text();
if (credName === "") {
throw 'No credentials available for EC2 instance';
}
resp = await ngx.fetch(EC2_IMDS_SECURITY_CREDENTIALS_ENDPOINT + credName, {
headers: {
'x-aws-ec2-metadata-token': token,
},
});
const creds = await resp.json();

return {
accessKeyId: creds.AccessKeyId,
secretAccessKey: creds.SecretAccessKey,
sessionToken: creds.Token,
expiration: creds.Expiration,
};
}

/**
* Get the credentials by assuming calling AssumeRoleWithWebIdentity with the environment variable
* values ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE and AWS_ROLE_SESSION_NAME
*
* @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['AWS_ROLE_SESSION_NAME'];

let sts_endpoint = process.env['STS_ENDPOINT'];
if (!sts_endpoint) {
/* On EKS, the ServiceAccount can be annotated with
'eks.amazonaws.com/sts-regional-endpoints' to control
the usage of regional endpoints. We are using the same standard
environment variable here as the AWS SDK. This is with the exception
of replacing the value `legacy` with `global` to match what EKS sets
the variable to.
See: https://docs.aws.amazon.com/sdkref/latest/guide/feature-sts-regionalized-endpoints.html
See: https://docs.aws.amazon.com/eks/latest/userguide/configure-sts-endpoint.html */
const sts_regional = process.env['AWS_STS_REGIONAL_ENDPOINTS'] || 'global';
if (sts_regional === 'regional') {
/* STS regional endpoints can be derived from the region's name.
See: https://docs.aws.amazon.com/general/latest/gr/sts.html */
const region = process.env['AWS_REGION'];
if (region) {
sts_endpoint = `https://sts.${region}.amazonaws.com`;
} else {
throw 'Missing required AWS_REGION env variable';
}
} else {
// This is the default global endpoint
sts_endpoint = 'https://sts.amazonaws.com';
}
}

const token = fs.readFileSync(process.env['AWS_WEB_IDENTITY_TOKEN_FILE']);

const params = `Version=2011-06-15&Action=AssumeRoleWithWebIdentity&RoleArn=${arn}&RoleSessionName=${name}&WebIdentityToken=${token}`;

const response = await ngx.fetch(sts_endpoint + "?" + params, {
headers: {
"Accept": "application/json"
},
method: 'GET',
});

const resp = await response.json();
const creds = resp.AssumeRoleWithWebIdentityResponse.AssumeRoleWithWebIdentityResult.Credentials;

return {
accessKeyId: creds.AccessKeyId,
secretAccessKey: creds.SecretAccessKey,
sessionToken: creds.SessionToken,
expiration: creds.Expiration,
};
}

/**
* Get the current timestamp. This timestamp will be used across functions in
* order for there to be no variations in signatures.
*
* @returns {Date} The current moment as a timestamp
*/
function Now() {
return NOW;
}

export default {
Now,
fetchCredentials,
readCredentials,
sessionToken,
writeCredentials
Expand Down
Loading