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
?
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:
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.
Scope
)We'll specify REGIONAL
, as the only other option is CLOUDFRONT
which is only for Amazon CloudFront distributions.
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.
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:
Name
)We'll just name it 'MyHeaderRule`.
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.
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.
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.
VisbilityConfig
)As above.
Finally, we need our actual rule statement - a ByteMatchStatement
.
It also has 4 things we need to define:
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.
PositionalConstraint
)We want an exact match (Exactly matches string in the console) so we will set this to EXACTLY
.
SearchString
or SearchStringBase64
)We will set SearchString
to FOOBAR
, as FOOBAR
is a base64-unencoded value.
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