17

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 front of APIG. It's a bit of a shame, especially since APIG now supports custom domains natively, but it should work.

Now to make the solution secure rather than just obscure, I want to enforce that the APIs can only be accessed through the Cloudfront distro. What is the best option to do this?

  • I was hoping to be able to use the 'Origin Access Identities' similar as for S3, but don't see how to do that.
  • If I could assign an IAM User (or role?) to the Cloudfront distro, I could use APIG IAM feature, but I don't see how this can be done.
  • I could require an API key in APIG, and pass it as a Origin Custom Header from Cloudfront. That could work, as long as we don't want to use API keys for some other purpose, so I'm not entirely happy about that.
  • A dummy (!) custom authorizer could be used, with the Token validation expression actually checking a secret that is passed as an Origin Custom Header from Cloudfront. Should work, it's more flexible, but a bit dirty... or not?

Any better ideas? Or perhaps "the right way" to do it exists but I overlooked it?

Community
  • 1
  • 1
Free Willaert
  • 1,139
  • 4
  • 12
  • 24
  • 1
    Bullets 1 and 2 are no-go. CloudFront uses origin access identities *instead* of IAM roles/users, but that only applies to S3... so bullets 3 and 4 are the winners. I'm ashamed to admit that although 4 was obvious to me, I didn't think of 3, which seems even better unless you want API keys. – Michael - sqlbot Apr 14 '17 at 23:22

4 Answers4

6

I am from API Gateway.

Unfortunately, the best solution we have as of now is, to inject an origin custom header in CloudFront and validate that in a custom authorizer (option 4 in your question).

We are already aware of this limitation and not-so-great workaround. We are looking to provide better WAF integration in future, but we do not have an ETA.

Balaji
  • 1,028
  • 8
  • 12
  • 4
    You have no idea how much trouble this has been to us. Please add WAF integration to API Gateway ASAP – sscarduzio Sep 08 '17 at 16:40
  • 3
    Now you can use Regional Domain Names and create your own CloudFront distribution fronting the Regional Domain Endpoint. You can setup WAF in your CloudFront distribution. For more details, see https://aws.amazon.com/about-aws/whats-new/2017/11/amazon-api-gateway-supports-regional-api-endpoints/ – Balaji Nov 03 '17 at 18:18
  • @Balaji I would appreciate more info on how this should be configured. I am doing exactly these things, WAF, CloudFront, APIG with REGIONAL. My DNS points at CloudFront and correctly blocks network traffic outside the WAF rules. However, I can still access the APIG endpoint from my phone on 4G... – Matt Canty Jan 15 '18 at 10:27
  • Did you use use a custom header in your CF distribution? If not, that's what you are missing. You will need to send a custom header with a value and use custom authorizer in API GW to verify that value. That way, only the requests via the CF distribution are authorized. The recommended approach would be to use Lambda@Edge and keep rotating the header value and use Lambda of custom authorizer to verify that. – Balaji Jan 16 '18 at 18:10
  • Is this roll-your-own security workaround still the only option? – Adrian Baker Nov 13 '18 at 03:27
  • https://aws.amazon.com/blogs/compute/protecting-your-api-using-amazon-api-gateway-and-aws-waf-part-2/ This is how i have implemented it – amittn Jan 13 '19 at 17:15
3

The "right" way would be to use the custom authorizor in API Gateway as mentioned by others.

The "cheap" way would be bullet 3, an api key. You would probably only provision waf -> cloudfront -> api gateway if you were trying to fend off a ddos attack. So if someone discovered your api gateway url and decided to ddos that instead of cloudfront, a custom authorizor means you are now taking the brunt of the attack on lambda. Api gateway can handle over 10k requests per second, the default lambda limit is 100 per second. Even if you got amazon to increase your limit are you willing to pay for 10k lambda's per second for a sustained attack?

AWS reps will tell you, "API Keys are for identification, not for authentication. The keys are not used to sign requests, and should not be used as a security mechanism" https://aws.amazon.com/blogs/aws/new-usage-plans-for-amazon-api-gateway/

But honestly if you are not going to do something better in your lambda than validate some giant jumbled string why not leave that burden and cost to someone else. (Max key length is 128 characters)

Maybe you could have a scheduled lambda function to issue a new api key and update cloudfront's header every 6 hours?

If you want to use api keys for other things then just have one api gateway origin for authentication, and another origin and api gateway for everything else. This way in a ddos attack you can handle 10k request per second to your auth api, while all other customers who are already logged in have a collective 10k per second to use your api. Cloudfront and waf can handle 100K per second so they won't hold you back in this scenario.

One other thing of note if you are using lambda behind api gateway, you could use lambda@edge and just skip api gateway all together. (This won't fit most scenarios because lambda@edge is severely limited, but I figured I would throw it out there.)

But ultimately WE NEED WAF INTEGRATION WITH API GATEWAY!! : )

Neo
  • 1,282
  • 12
  • 12
  • Thanks for your response. Regarding the fact that custom authorizer would mean that lambda gets all the hits: custom authorizer responses can be cached, so only one lambda request (still dummy) would be made every TTL seconds, right? See http://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html#configure-api-gateway-custom-authorization-with-console – Free Willaert Sep 11 '17 at 07:06
  • Authenticated users are cached. Someone ddosing your system will just create random tokens with each request. https://stackoverflow.com/questions/44687654/secure-aws-api-gateway-with-lambda-integration. This forum dives pretty deep into the subject too: https://forums.aws.amazon.com/thread.jspa?messageID=703917 – Neo Sep 11 '17 at 21:34
  • The lambda is dummy - the actual check is done by the token validation expression. Lambda only gets called when cache is not hit *and* validation expression succeeds. Right? – Free Willaert Sep 12 '17 at 18:45
  • Funny thing is, that's what Amazon seems to recommend: https://aws.amazon.com/fr/blogs/compute/protecting-your-api-using-amazon-api-gateway-and-aws-waf-part-2/ – ElFitz Oct 30 '18 at 14:35
1

You can use custom domain name and point DNS to the distribution with WAF. Requests directly to the original API Gateway distribution will not work then.

  • 1
    That's not quite right, since there's a conflict between the custom domain name being associated with the hidden CloudFront distribution associated with the API itself, and with the same custom domain name being associated with the CloudFront distribution you create and point DNS to. Only one CloudFront distribution, globally, can be associated with a given domain name. – Michael - sqlbot Apr 14 '17 at 23:18
  • @Michael More or less aside: I never realized that, but that's good to know. For the records, trying this results in "com.amazonaws.services.cloudfront.model.CNAMEAlreadyExistsException: One or more of the CNAMEs you provided are already associated with a different resource. (Service: AmazonCloudFront; Status Code: 409; Error Code: CNAMEAlreadyExists; ..." – Free Willaert Apr 15 '17 at 05:58
  • 2
    @Vitalii Even though it might not work, I must say I don't see your point: if you create a Custom Domain Name in APIG, you can still call the API directly through it's https://xxx.execute-api.eu-west-1.amazonaws.com endpoint, right? – Free Willaert Apr 15 '17 at 06:01
  • @FreeWillaert you are right. I am experiencing this now. I have WAF -> CloudFront -> APIG. The WAF is correctly denying all traffic via the CloudFront dist but my API is still accessible via the APIG generated endpoint. Pretty frustrating considering how much effort was put into working out how to do this. – Matt Canty Jan 15 '18 at 10:25
1

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

Reed Hermes
  • 1,958
  • 4
  • 16
  • 22