Search code examples
amazon-web-servicesaws-lambdaaws-api-gatewayserverless-framework

Can I validate a header value in API Gateway?


I use the Serverless Framework to build and deploy my lambdas.

There's a service I use that sends a secret string in a specific header e.g. X-Secret, and this string is always the same e.g. FOOBAR.

I don't want to create a Lambda authoriser just to compare it.

Can API Gateway natively filter out requests based on header values?

In this case, can it block requests that don't have the header X-Secret set to FOOBAR?


Solution

  • Can API Gateway natively filter out requests based on header values?

    Unfortunately, Amazon API Gateway doesn't support natively checking header values. The closest feature would be API Gateway request validators which won't work as they can only check if the header exists, and not the header value.

    The only other option I can think of is using AWS WAF, which only supports REST APIs and has an 8 KB limit:

    You can also create rules that match a specified string or a regular expression pattern in HTTP headers, method, query string, URI, and the request body (limited to the first 8 KB).

    If possible, I would recommend just going with a simple Python Lambda authoriser. WAF would probably be more expensive and overkill.

    import json
    
    def lambda_handler(event, context):
        token = event['headers']['X-Secret']
        if token == 'FOOBAR':
            return generate_policy('user', 'Allow', event['methodArn'])
        else:
            return generate_policy('user', 'Deny', event['methodArn'])
    
    def generate_policy(principal_id, effect, resource):
        policy = {
            "principalId": principal_id,
            "policyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Action": "execute-api:Invoke",
                        "Effect": effect,
                        "Resource": resource
                    }
                ]
            }
        }
        return policy
    

    However, for whatever reason, you may still want to go ahead with AWS WAF.

    You need to define a web ACL, associate it with your API Gateway and create a suitable rule for your web ACL.

    We'll do this in AWS CloudFormation (CFN) as the Serverless framework is basically a wrapper around CFN. Anything that can be defined in CloudFormation is supported by the Serverless Framework.

    Two options:

    • Configure web ACL to allow by default & create a rule with a condition to block any request that doesn't have the header X-Secret set to FOOBAR

    • Configure web ACL to block by default & create a rule with a condition to allow any request that does have the header X-Secret set to FOOBAR (objectively easier logic to visualise and implement)

    We'll go with option 2.


    We need a web ACL first.

    Essentially, it's a container for rules. The Web ACL as a whole governs how your AWS resource i.e. your API Gateway responds to web requests based on the rules it contains.

    It needs 4 things to be defined:

    1. What is this web ACL called? (Name)

    We'll just call it MyAcl. Note the CloudFormation docs are incorrect in saying it is not required and I've submitted a feedback request to fix this - it is needed by the API and thus CloudFormation.

    1. What is this web ACL for? (Scope)

    We'll specify REGIONAL, as the only other option is CLOUDFRONT which is only for Amazon CloudFront distributions.

    1. If none of the defined rules match, what's the default action? (DefaultAction)

    In this case, we're going with a single rule that allows anything with the X-Secret set to FOOBAR so we want to block everything else. We'll set this to BlockAction to block requests that don't match our rule by default. If you went with the inverse scenario, you'd need to inverse this.

    1. Should metrics be sent to Amazon CloudWatch and/or should requests be sampled? (VisbilityConfig)

    In this case, we need neither of these features so we'll set CloudWatchMetricsEnabled and SampledRequestsEnabled both to false. We also need to weirdly still set MetricName to something even if it's disabled (forever a mystery) so we'll set it to RandomMetricName.

    You also will eventually define Rules, a list of Rule resources. Pretty self-explanatory in that it will contain your rule(s) but this isn't strictly needed at the time of creation.


    Then, we need a WAF rule, as we have a container but no stuff.

    A WAF rule lets you precisely target the requests that you want to allow or block by specifying the exact condition(s) that you want it to watch for.

    It needs 5 things to be defined:

    1. What's the name of this rule? (Name)

    We'll just name it 'MyHeaderRule`.

    1. What is the priority of this rule? (Priority)

    In this case, we have one rule so we could set this to any number. We'll just set this to 0 but note that when you have more than one rule, priorities are really important as they determine processing order.

    1. What is this rule doing? (Statement)

    Our rule statement will define a string match search on the X-Secret header. The AWS WAF console & the developer guide call it a string match rule statement while the API and CloudFormation call it a ByteMatchStatement.

    1. Should AWS WAF allow or block the request on a rule match? (Action)

    We'll set this to AllowAction to allow on rule match, as the ACL will block everything else based on our DefaultAction. Note for completeness sake, this doesn't always need to be set depending on if you're using rule groups or not, and sometimes you might need to set OverrideAction but in this case, we do need it.

    1. Should metrics be sent to Amazon CloudWatch and/or should requests be sampled? (VisbilityConfig)

    As above.


    Finally, we need our actual rule statement - a ByteMatchStatement.

    It also has 4 things we need to define:

    1. What should the statement check a match against? (FieldToMatch)

    We want to check (a header in) the headers. However, as per the docs for Headers, since we only want to inspect a single header, we have a mini shortcut of just setting this to a SingleHeader object with a Name value of X-Secret. This is easier than defining a more complex Headers object.

    1. Where should the statement check for a match or more informally, what is the search type? (PositionalConstraint)

    We want an exact match (Exactly matches string in the console) so we will set this to EXACTLY.

    1. What is the value for matching to be done against? (SearchString or SearchStringBase64)

    We will set SearchString to FOOBAR, as FOOBAR is a base64-unencoded value.

    1. Should AWS WAF perform any transformation on the value of FieldToMatch, before it checks for a match? (TextTransformations)

    As per docs, this is usually done to reverse unusual formatting that attackers use in web requests in an effort to bypass detection. In this case, we don't want any transformations so we'll just set our Type to NONE and Priority to 0.

    Our rule in JSON would look like this:

    {
      "Name": "MyHeaderRule",
      "Priority": 0,
      "Action": {
        "Allow": {}
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": false,
        "CloudWatchMetricsEnabled": false,
        "MetricName": "RandomMetricName"
      },
      "Statement": {
        "ByteMatchStatement": {
          "FieldToMatch": {
            "SingleHeader": {
              "Name": "X-Secret"
            }
          },
          "PositionalConstraint": "EXACTLY",
          "SearchString": "FOOBAR",
          "TextTransformations": [
            {
              "Type": "NONE",
              "Priority": 0
            }
          ]
        }
      }
    }
    

    Bringing all of the above together with the serverless-associate-waf plugin, we get something like this:

    plugins:
      - serverless-associate-waf
    
    custom:
      wafAssociation:
        name: ${self:resources.Resources.WAFWebACL.Properties.Name}
        version: V2
    
    resources:
      WAFWebACL:
      Type: 'AWS::WAFv2::WebACL'
      Properties:
        Name: MyAcl
        Scope: REGIONAL
        DefaultAction:
          Block: {}
        VisibilityConfig:
          SampledRequestsEnabled: false
          CloudWatchMetricsEnabled: false
          MetricName: RandomMetricName
        Rules:
          - Name: MyHeaderRule
            Priority: 0
            Statement:
              ByteMatchStatement:
                FieldToMatch:
                  SingleHeader:
                    Name: X-Secret
                PositionalConstraint: EXACTLY
                SearchString: FOOBAR
                TextTransformations:
                  - Priority: 0
                    Type: NONE
            Action:
              Allow: {}
            VisibilityConfig:
              SampledRequestsEnabled: false
              CloudWatchMetricsEnabled: false
              MetricName: RandomMetricName