Lambda Function URL

2022-05-15

Support to call a Lambda Function via a public url was released in April 2022. It comes with a built in HTTPS endpoint good for single function microservices. In the first part of this blog we will configure and create a Lambda Function with function URLs enabled, in the second part we will look at some security considerations.

Let's start by heading over to the AWS Lambda part of the Console and click Create Function. We'll base the creation on the Hello World blueprint. image

Click Configure and give the function a name, I call mine hello-world-lambda-url, select to create a new IAM Role, leave everything else as default and click Create function.

Navigate over to the function and update the code to be

console.log('Loading function');

exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));

return {
statusCode: 200,
body: "Hello from Lambda URL"
}
};

You can test you function to make sure it returns a status 200 and a message.

Now let's configure the Lambda url so we can call the function. Open up the function in the console, if not already open, click on Configure tab and select Function URL to the left image

Click on Create function URL, set Auth type to NONE and leave the default resource policy image

Now you should have a configuration that looks like this, with a unique URL for your function. image

Click the arrow next to the URL to open it in the browser, you should see a message Hello from Lambda URL

We can also do a test in Postman and to see more details if we like. image

We are done! It's not possible to call our Lambda Function over a basic HTTPS endpoint, it was a fairly easy setup. But we are not done, now lets do some modifications before we look at some extra security.

Update Function Code

Before we move on we will simulate that we actually do some work in the function. Let's add some simple function that will wait some time to simulate data processing.

console.log('Loading function');

exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));

await processData();

return {
statusCode: 200,
body: "Hello from Lambda URL"
}
};

function processData() {
return new Promise(resolve => setTimeout(resolve, 2000));
};

Since we have a three second timeout make sure you update that if you use a wait time larger than that. Calling the endpoint in either Postman or the browser we can see that it takes the function around 2 seconds to return, that is exactly what was expected. We are now doing some work and the function take some extra time to respond. Let us do one final change and add json data instead of a string in the return. So once again I update the function.

console.log('Loading function');

exports.handler = async (event, context) => {
console.log('Received event:', JSON.stringify(event, null, 2));

await processData();

return {
statusCode: 200,
body: "Hello from Lambda URL"
}
};

function processData() {
return new Promise(resolve => setTimeout(resolve, 2000));
};

If we now load the function in Postman or the browser we see a result, '{"received":true,"processedData":"Hello there"}'

All good, let's move to the security considerations.

Security considerations

With our Function URL configured and available to call, let's create a small python script that will call the function 100 times rapidly with every call being made from a new Thread. This is not a problem for AWS Lambda, it will just scale out to meet the load.

import json
import requests
import threading


def run_in_thread():
url = "https://zyc5mwsmzsifcsedx4s6eaqqru0zveen.lambda-url.eu-west-1.on.aws/"
headers = {'SignatureHeader': 'XYZ', 'Content-type': 'application/json'}
payload = json.dumps({'type': 'payment-succeeded'})
querystring = {'myCustomParameter': f'squirrel-{x}'}

r = requests.post(url=url, params=querystring,
data=payload, headers=headers)
print(r.json())


for x in range(100):
x = threading.Thread(target=run_in_thread)
x.start()
print("Thread started!")

Checking the metrics in CloudWatch we can clearly see that we have 100 invocations done in a very short period of time. image

And if we check the metrics for the concurrent invocations we can see that this is also at 100 as expected, as we run for two seconds and invoke it almost at the same time. image

Now that is not a problem right? Lambda is serverless and just scaled out to meet the load put on it. Well, this can actually be a big problem. Remember that the Function URL is public, basically anyone can call it, and Security through obscurity is not that good of security.

By default our AWS Account have a limit of 1000 concurrent Lambda invocation, so imagine that if someone would start calling our function over and over and over again as an attack, the attacker could quickly prevent other Lambda functions in our account to run. If our system then is heavily depending on Lambda we could quickly experience an outage.

So what can we do about it then? Luckily it's possible to set a reserved concurrency that, despite the name, will only allow for the specified number of concurrent invocations. Let us configure that and rerun our test, move to Configuration tab and select Concurrency to the left. image

Click on Edit button for the Concurrency, top right. Let us put in a new value of 10. image

Click Save and we should have a new configuration looking like this. image

Now let us rerun our test and see what happens. Rather quickly during the test run we start to see Rate Exceeded messages '{'message': 'Rate Exceeded.'}' that is a indicator that the concurrency setting is throttling our Lambda function. Looking at the CloudWatch metrics we see that our function has been throttled 90 times, which we expected. image

And if we also look at the concurrent number of invocations we can see that that is now 10. image

So with this setting we have at least blocked any attacker from starving our other Lambda functions. However if the function that is callable via the public endpoint is crucial for our system the attacker could still create problems.

The better option would be to not allow it to be public in the first place, and use AWS_IAM as the auth method.

Blocking the configuration

If you like to block the usage of Lambda URLs that can be accomplished using a Service Control Policy (SCP) in AWS Organizations. Thanks to Ben Kehoe for the SCP

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"lambda:CreateFunctionUrlConfig",
"lambda:UpdateFunctionUrlConfig"
],
"Resource": "arn:aws:lambda:*:*:fucntion:*",
"Condition": {
"StringNotEquals": {
"lambda:FunctionUrlAuthType": "AWS_IAM"
}
}
}
]
}

Conclusion

The Lambda Function URL is a feature that can be useful in some circumstances, where you like to call the Lambda function over plain HTTPS endpoint without using a API Gateway. It can be good for single function microservices. However everyone need to be aware that is do come with some security concerns. Before using the feature be sure that you analyze and do conscious choices.

My recommendation would be to make sure you always use Auth method AWS_IAM if you plan to use this feature.