I want to put WAF in front of API Gateway, and with the (little) info I find that is only possible by manually putting an extra Cloudfront distribution with WAF enabled, in fron
It is possible to force access through CloudFront via using a Lambda@Edge function for SigV4 signing origin requests and then enabling IAM auth on your API Gateway. This strategy is able to be used in conjunction with API Keys in your CloudFront distribution (guide for CloudFront+API Key).
Assuming that you have already setup API Gateway as an origin for your CloudFront distribution, you first need to create a Lambda@Edge function (guide for Lambda@Edge setup) and then ensure its execution role has access to the API Gateway that you would like to access. For simplicity, you can use the AmazonAPIGatewayInvokeFullAccess
managed IAM policy in your Lambda's execution role which gives it access to invoke any API Gateway within your account.
Then, if you go with using aws4 as your signing client, this is what your Lambda@Edge code would look like:
const aws4 = require("aws4");
const signCloudFrontOriginRequest = (request) => {
const searchString = request.querystring === "" ? "" : `?${request.querystring}`;
// Utilize a dummy request because the structure of the CloudFront origin request
// is different than the signing client expects
const dummyRequest = {
host: request.origin.custom.domainName,
method: request.method,
path: `${request.origin.custom.path}${request.uri}${searchString}`,
};
// Include the body in the signature if present
if (Object.hasOwnProperty.call(request, 'body')) {
const { data, encoding } = request.body;
const buffer = Buffer.from(data, encoding);
const decodedBody = buffer.toString('utf8');
if (decodedBody !== '') {
dummyRequest.body = decodedBody;
dummyRequest.headers = { 'content-type': request.headers['content-type'][0].value };
}
}
// Use the Lambda's execution role credentials
const credentials = {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
sessionToken: process.env.AWS_SESSION_TOKEN
};
aws4.sign(dummyRequest, credentials); // Signs the dummyRequest object
// Sign a clone of the CloudFront origin request with appropriate headers from the signed dummyRequest
const signedRequest = JSON.parse(JSON.stringify(request));
signedRequest.headers.authorization = [ { key: "Authorization", value: dummyRequest.headers.Authorization } ];
signedRequest.headers["x-amz-date"] = [ { key: "X-Amz-Date", value: dummyRequest.headers["X-Amz-Date"] } ];
signedRequest.headers["x-amz-security-token"] = [ { key: "X-Amz-Security-Token", value: dummyRequest.headers["X-Amz-Security-Token"] } ];
return signedRequest;
};
const handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const signedRequest = signCloudFrontOriginRequest(request);
callback(null, signedRequest);
};
module.exports.handler = handler;
Note that if you you include a body in your request, you have to manually configure your Lambda@Edge function to include the body via the console or SDK or setup a CloudFormation custom resource to call the SDK since CloudFormation does not support enabling this natively yet