Written by Arunava Mukherjee, Operations Manager
Some organizations require you use SSE-KMS encryption on your S3 buckets and use CloudFront to deliver objects. In this section, you will learn how to serve content encrypted with SSE-KMS from S3 using CloudFront. Then, learn to use Lambda@Edge, a feature of CloudFront, to code custom logic on your CloudFront distribution using Javascript. Your Lambda@Edge functions are given IAM permissions to read from S3 and indirectly operate encryption/decryption using a CMK managed by KMS. These functions are triggered every time CloudFront makes a request to S3, and sign the request with AWS Signature Version 4 by adding the necessary headers. This signed request allows CloudFront to retrieve your object encrypted with SSE-KMS.
- Check theServer-side encryptionattribute of this object in the Overview tab, and verify that it was encrypted by default by S3 with the KMS CMK.
- If you test the object URL using CloudFront, access is denied. We have not yet created the Lambda@Edge function that signs requests to S3, and allows CloudFront to retrieve the object.
Create the Lambda@Edge function
Although Lambda@Edge runs on CloudFront’s global network, you must create the function in the N. Virginia Region (us-east-1). Go to the AWS Lambda console in us-east-1 and create a new function with the Node.js 12 runtime.
Navigate to the created IAM role and attach the AWS managed policy named AmazonS3ReadOnlyAccess to the role. This allows the role (and the function) to sign requests to S3. You can enforce more restrictive permissions by allowing read access only to the specific bucket created in the stack. For more, read the documentation on security best practices with S3.
Copy the following code and paste it in the function using the embedded IDE in the Lambda console UI.
This function will convert the S3 origin to Custom origin on the fly and sign the s3 request with signed header.
'use strict';
//Declare for origin change
const querystring = require('querystring');
// Declare constants reqiured for the signature process
const crypto = require('crypto');
const emptyHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
const signedHeaders = 'host;x-amz-cf-id;x-amz-content-sha256;x-amz-date;x-amz-security-token';
// Retrieve the temporary IAM credentials of the function that were granted by
// the Lambda@Edge service based on the function permissions. In this solution, the function
// is given permissions to read from S3.
const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN } = process.env;
// Since the function is configured to be executed on origin request events, the handler
// is executed every time CloudFront needs to go back to the origin, which is S3 here.
exports.handler = async event => {
//console.log(JSON.stringify(event));
// Retrieve the original request that CloudFront was going to send to S3
const request = event.Records[0].cf.request;
const host_part = (request.headers['host'][0].value).split('.');
const s3region = 'ap-south-1';
//Changing s3 origin to custom origin
const params = querystring.parse(request.querystring);
if (!(params['useCustomOrigin'])) {
/* Set custom origin fields*/
request.origin = {
custom: {
//domainName: request.headers['host'][0].value,
domainName: `${host_part[0]}.${host_part[1]}.${s3region}.${host_part[2]}.${host_part[3]}`,
port: 443,
protocol: 'https',
path: '',
sslProtocols: ['TLSv1'],
readTimeout: 5,
keepaliveTimeout: 5,
customHeaders: {}
}
};
request.headers['host'] = [{ key: 'host', value: request.origin.custom.domainName }];
}
// Create a JSON object with the fields that should be included in the Sigv4 request,
// including the X-Amz-Cf-Id header that CloudFront adds to every request forwarded
// upstream. This header is exposed to Lambda@Edge in the event object
const sigv4Options = {
method: request.method,
path: request.origin.custom.path + request.uri,
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
sessionToken: AWS_SESSION_TOKEN
},
host: request.headers['host'][0].value,
xAmzCfId: event.Records[0].cf.config.requestId
};
//console.log(sigv4Options);
// Compute the signature object that includes the following headers: X-Amz-Security-Token, Authorization,
// X-Amz-Date, X-Amz-Content-Sha256, and X-Amz-Security-Token
const signature = signV4(sigv4Options);
// Finally, add the signature headers to the request before it is sent to S3
for(var header in signature){
request.headers[header.toLowerCase()] = [{
key: header,
value: signature[header].toString()
}];
}
//console.log(JSON.stringify(event));
return request;
};
// Helper functions to sign the request using AWS Signature Version 4
// This helper only works for S3, using GET/HEAD requests, without query strings
// https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
function signV4(options) {
// Infer the region from the host header
const region = options.host.split('.')[2];
//const region = 'ap-south-1';
// Create the canonical request
const date = (new Date()).toISOString().replace(/[:-]|\.\d{3}/g, '');
const canonicalHeaders = ['host:'+options.host,'x-amz-cf-id:'+options.xAmzCfId,'x-amz-content-sha256:'+emptyHash, 'x-amz-date:'+date, 'x-amz-security-token:'+options.credentials.sessionToken].join('\n');
const canonicalURI = encodeRfc3986(encodeURIComponent(decodeURIComponent(options.path).replace(/\+/g, ' ')).replace(/%2F/g, '/'));
const canonicalRequest = [options.method, canonicalURI, '', canonicalHeaders + '\n', signedHeaders,emptyHash].join('\n');
// Create string to sign
const credentialScope = [date.slice(0, 8), region, 's3/aws4_request'].join('/');
const stringToSign = ['AWS4-HMAC-SHA256', date, credentialScope, hash(canonicalRequest, 'hex')].join('\n');
// Calculate the signature
const signature = hmac(hmac(hmac(hmac(hmac('AWS4' + options.credentials.secretAccessKey, date.slice(0, 8)), region), "s3"), 'aws4_request'), stringToSign, 'hex');
// Form the authorization header
const authorizationHeader = ['AWS4-HMAC-SHA256 Credential=' + options.credentials.accessKeyId + '/' + credentialScope,'SignedHeaders=' + signedHeaders,'Signature=' + signature].join(', ');
// return required headers for Sigv4 to be added to the request to S3
return {
'Authorization': authorizationHeader,
'X-Amz-Content-Sha256' : emptyHash,
'X-Amz-Date': date,
'X-Amz-Security-Token': options.credentials.sessionToken
};
}
function encodeRfc3986(urlEncodedStr) {
return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
}
function hash(string, encoding) {
return crypto.createHash('sha256').update(string, 'utf8').digest(encoding);
}
function hmac(key, string, encoding) {
return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding);
}
Finally, deploy this function to the CloudFront distribution for origin request events by clicking on the Actions menu, then the Deploy to Lambda@Edge option. Make sure that you use the distribution created by the CloudFormation stack. The origin request event is triggered every time CloudFront makes a request upstream to the origin, in this case S3.
Required KMS Key Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::105537364995:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow access through S3 for all principals in the account that are authorized to use S3",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:CallerAccount": "105537364995",
"kms:ViaService": "s3.ap-south-1.amazonaws.com"
}
}
}
]
}