How to prevent public AWS Lambda abuse using API Gateway
The Risks of Public AWS Lambda Functions
Public AWS Lambda functions are powerful, but without proper safeguards, they can be misused. Here’s what can go wrong:
Open Access: If anyone can trigger your Lambda function, it might be used for the wrong reasons, leading to overuse or sensitive data exposure.
Cost Issues: Lambda charges are based on usage. If someone repeatedly triggers your function, your bills could skyrocket.
Data Theft: A Lambda function dealing with sensitive data can be a target for data leaks if not secured properly.
Service Disruption: Excessive traffic, whether intentional or not, can overload your Lambda functions, disrupting the service.
In this guide I'll focus on #2.
The steps
Here're the steps in order we'll do.
Create a Lambda function + test
Add an API gateway endpoint to it
Create an API key with limited usage on it + test
The Lambda function
First we need to create an example Lambda function:
Let's fill in the lambda function with code that adds a unix timestamp to the request JSON:
import json
import time
def lambda_handler(event, context):
# Parsing the JSON body from the event
data = json.loads(event['body'])
# Append the current Unix timestamp
data['timestamp'] = int(time.time())
# Return the modified data as JSON
return {
'statusCode': 200,
'body': json.dumps(data)
}
Click Deploy.
Once done, you can add a new test event and test with this example that imitates an API gateway JSON:
{
"resource": "/formsubmit",
"path": "/formsubmit",
"httpMethod": "POST",
"headers": {
"Content-Type": "application/json",
"Accept": "application/json"
},
"queryStringParameters": null,
"multiValueQueryStringParameters": null,
"pathParameters": null,
"stageVariables": null,
"requestContext": {
"requestTime": "30/Jan/2024:12:31:45 +0000",
"path": "/prod/formsubmit",
"protocol": "HTTP/1.1",
"stage": "prod",
"domainName": "api.example.com",
"requestId": "123456789",
"requestTimeEpoch": 1580389905401,
"accountId": "123456789012",
"apiId": "abcdefghij"
},
"body": "{\"name\": \"John Doe\", \"email\": \"johndoe@example.com\"}",
"isBase64Encoded": false
}
In the execution results you should see something like this, having the timestamp added:
Response
{
"statusCode": 200,
"body": "{\"name\": \"John Doe\", \"email\": \"johndoe@example.com\", \"timestamp\": 1706616281}"
}
API Gateway
At this point you have a lambda function that can be used within AWS.
In order to call it from outside your architecture, you can have an API Gateway endpoint triggering your Lambda function.
Something like this:
API gateway will give us the power to restrict the incoming calls using API keys.
So as a next step...create a trigger in your lambda function and select API gateway.
Create a REST Api:
Then visit your newly created API.
Deploy this to any stage, I used default, then take a note of the invoke URL that's something similar to this, and append your function name to it:
https://123456asdfg.execute-api.us-east-1.amazonaws.com/default/timestamper
You can use this URL to test your endpoint.
Here's the fun part, let's try it!
You can curl the endpoint from any terminal:
curl -X POST "https://12345asdfg.execute-api.us-east-1.amazonaws.com/default/timestamper" \
-H "Content-Type: application/json" \
-d "{\"name\": \"John Doe\", \"email\": \"johndoe@example.com\"}"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 129 100 77 100 52 180 122 --:--:-- --:--:-- --:--:-- 304{"name": "John Doe", "email": "johndoe@example.com", "timestamp": 1706626702}
As you see the response has come back and now our gateway endpoint is public 😱
What we need to do now is to limit the access of it via API key.
Gateway API keys
Within API Gateway => APIs => API keys => Create API
key section, create an API key. You only need to add a name to it and autogenerate it.
Similarly, just above that, you can add a API Gateway => APIs => Usage plan => Create usage plan
And this is where the magic happens.
Right here you can restrict the API in various ways, as an example:
This will provide
10 calls a day with this API key
max. 1 call a second
max. 1 call at a time
Of course it is fairly restrictive, but this will prevent anyone calling your 100s of times a second racking up a nice Lambda and API Gateway bill for you.
Once done, go back to your API key and add it to the usage plan. You can find this option under the "Actions" button.
You can now go back to your API and modify the resource with the EDIT button:
Once clicked, you can turn on "Api key required", then save.
Before you re-try your curl, make sure you "Deploy API" and wait a few minutes.
After that, your response should be something like this:
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 75 100 23 100 52 59 133 --:--:-- --:--:-- --:--:-- 193{"message":"Forbidden"}
Great! Now our endpoint requires an API key.
That's great, but how do I send the API key? 🤔
Well, here's how:
curl -X POST "https://12345asdfg.execute-api.us-east-1.amazonaws.com/default/timestamper2" \
-H "Content-Type: application/json" \
-H "x-api-key: 1234567890asdfghjkl" \
-d "{\"name\": \"John Doe\", \"email\": \"johndoe@example.com\"}"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 129 100 77 100 52 138 93 --:--:-- --:--:-- --:--:-- 232{"name": "John Doe", "email": "johndoe@example.com", "timestamp": 1706632463}
If we send request too quickly we'll get this response:
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 83 100 31 100 52 71 120 --:--:-- --:--:-- --:--:-- 193{"message":"Too Many Requests"}
And if we run out of the daily limit we'll get this:
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 80 100 28 100 52 81 151 --:--:-- --:--:-- --:--:-- 234{"message":"Limit Exceeded"}
Closing thoughts
There's of course plenty of other ways to secure your lambda function, to name a few:
AWS WAF for an extra security layer. It’s like having a guard to block unwanted traffic. With this, you can selectively deny traffic.
Control Access with IAM who can use your Lambda functions.
Use CloudWatch for detailed logging and alerts.
Set limits on how many instances of your Lambda function can run at once via Lambda concurrency. This prevents your system from getting overwhelmed by too much traffic.
Subscribe to my newsletter
Read articles from Kris F directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Kris F
Kris F
I am a dedicated double certified AWS Engineer with 10 years of experience, deeply immersed in cloud technologies. My expertise lies in a broad array of AWS services, including EC2, ECS, RDS, and CloudFormation. I am adept at safeguarding cloud environments, facilitating migrations to Docker and ECS, and improving CI/CD workflows with Git and Bitbucket. While my primary focus is on AWS and cloud system administration, I also have a history in software engineering, particularly in Javascript-based (Node.js, React) and SQL. In my free time I like to employ no-code/low code tools to create AI driven automations on my self hosted home lab.