This article is part of a two-part series:
- Part one - high-level overview, general security, frontend security
- Part two - backend security
To catch you up on what was covered in Part One, here is a brief summary of the last article.
Serverless has made security much easier. However, you still need to devote energy to make your applications secure. Finally, a lot of security exploits with serverless related technologies come from misconfiguration which exposes non-public information publicly. It’s been a problem long before serverless, but now we can simply press a couple of buttons and let the fully managed power of serverless technologies do most of the work.
How do I secure serverless applications?
Okay so let’s imagine a common serverless application then go through how we should think about security.
Our application will be broken into a frontend and a backend.
The frontend consists of the following resources:
- S3 Bucket - website file storage (e.g. index.html, js/, css/)
- CloudFront Distribution - CDN (https, performance)
The backend consists of the following resources:
- Lambda functions - business logic
- API Gateway - REST API
- DynamoDB - NoSQL database
What about our AWS accounts:
- DEV - sandbox account for developers to develop new features
- QA - test new features
- PROD - user-facing
Now that you have some refreshed context, let’s jump into how to secure your serverless application backends. We will kick this section off with how to secure your AWS Lambda functions.
Lambda functions are the bread and butter of our serverless applications typically referred to as “glue code”. Lambda functions connect to services throughout our AWS account so we will want to make sure that they are locked down.
Way to wide permissions
A common mistake is to have to wide AWS IAM permissions. This results in the Lambda functions being able to access more than they need which can open up security vulnerabilities depending on the nature of the lambda function.
The typical rule of thumb is to use the principle of least privilege, meaning you can only access exactly what you need and nothing else.
The best way to manage secrets with AWS Lambda is to pull them in during runtime. If you fail to do this, you’re going to most likely be exposing your secrets via the AWS Lambda console as plain-text environment variables.
If you’re not using AWS Secrets Manager and pulling these secrets in during runtime then you should take the time to research how to implement this. It should only be a few lines and a helper function leveraging the aws-sdk. The result will be the most secure way to handle secrets as the secrets are not exposed via environment variables.
When it comes to securing API Gateway we have a couple of core areas. Let’s focus on the concept of authorizers.
Source:API Gateway Authorizer Docs
In the image above we are using an AWS Lambda function to act as our “custom authorizer” this means that all requests to our secured endpoints will first automatically be passed to an “authorizer” lambda which will then inspect the request, execute some custom business logic to validate the token being passed and generate an AWS IAM Policy, and then finally deny or allow the request to continue.
If your AWS Lambda custom authorizer returns a success then AWS API Gateway will use that AWS IAM policy to allow or deny access to your AWS resources.
This type of flow is useful when you already have a third party user pool that you want to reuse and still authenticate your serverless backend against versus using something like AWS Cognito.
Cognito User Pool Authorizer
A Cognito User Pool Authorizer is a simple way to build authentication into your applications without needing to code custom logic like in the Lambda Authorizer example.
We are effectively going to do the following steps:
- Create an AWS Cognito User Pool and Cognito User Pool Client
- Attach our AWS Cognito User Pool as an “authorizer” to our API Gateway instance and specify the header name (e.g. x-api-key or Authorization)
That’s pretty much all of it. If your frontend can already handle register, login, etc. then when your users log in using your AWS Cognito User Pool they will get back a JWT token which all future requests will pass as a specific header.
AWS IAM Authorization
The third method we will talk about is AWS IAM authorization. We can use AWS IAM authorization to give granular permissions to users once authenticated in our applications and still leverage AWS Cognito with the addition of an AWS Cognito Identity Pool.
When working with AWS IAM authorization, leveraging a frontend package like AWS Amplify can make this a lot easier.
AWS IAM authorization is great for a few reasons, we pointed out the granular access control, but you also can handle both unauthenticated and authenticated users. Something that is not possible with AWS Cognito.
What do I choose?
Well, this is going to widely depend on your existing environment and application-specific context. However, at a high level.
Use Custom Lambda authorizer when you’re integrating existing systems e.g. not AWS Cognito and when you need to do some custom business logic which is not supported by AWS Cognito directly.
Use an AWS Cognito User Pool authorizer when you’re looking for simple authentication with the least amount of overhead.
Use AWS IAM when you’re already using an AWS Cognito User Pool to manage users and looking to leverage granular access control for both authenticated and unauthenticated users.
When it comes to AWS DynamoDB we are going to want to first review the AWS posture on security with DynamoDB. To summarize, AWS has a shared responsibility model which basically means AWS will handle some security for you (e.g. security of the data center running the server which has your DynamoDB table) and you will handle the rest (e.g. access to DynamoDB via AWS IAM permissions).
“Amazon DynamoDB provides a highly durable storage infrastructure designed for mission-critical and primary data storage. Data is redundantly stored on multiple devices across multiple facilities in an Amazon DynamoDB Region.
DynamoDB protects user data stored at rest and also data in transit between on-premises clients and DynamoDB, and between DynamoDB and other AWS resources within the same AWS Region.” - Docs
As you can ascertain AWS is handling a lot of the security aspects on your behalf, automatically.
However, the one point which you need to ensure you are handling properly is scoping down your AWS IAM permissions to be the least privilege and that requests to your database have conditions on each query/mutation to match user data against a unique id.
For example, let’s imagine that we are using AWS IAM authentication backed by an AWS Cognito User Pool. Let’s also imagine that all of our data is being tied to a field called userId which is the same as our AWS Cognito User Pool user id.
Then we will add a condition statement to each DynamoDB request and in that condition, the value will automatically be populated by reading the logged in users JWT token. This ensures we are not exposing data to other users unless they “own” that data.
Okay, that’s all for the backend security related to AWS Lambda, AWS API Gateway, and AWS DynamoDB. However, there is one other area to strongly consider when thinking about serverless security and that would be a concept called whitelisting and blacklisting.
Whitelisting and Blacklisting with Thundra
When building serverless applications a common area that slips through the cracks is limiting the inner API calls being made inside your AWS Lambda functions. From the outside, you can limit with AWS IAM permissions, but that only has an impact on AWS resource access.
What about if you wanted to block all API URLs except for the ones that are approved by your security team? This would require some extra tooling on top of AWS Lambda to inspect what requests are being made inside of your AWS Lambda function then take some action based on what you defined.
You can set this up pretty easily with Thundra. Here are the high-level steps:
- Create a Thundra account
- Connect your AWS Account
- Navigate to security configuration settings
Now let’s dive into whitelisting and blacklisting individually.
Source: Thundra Docs
As you can see in the image above we can see the existing AWS Lambda permissions and then we are simply adding a new whitelisted resource that will allow all HTTP requests to www.google.com.
Great now let’s look at blacklisting.
Source: Thundra Docs
In the image above we can see again that we have a few existing AWS IAM permissions on our AWS Lambda function. You can also see that we added a blacklisted resource which will block ALL READ requests to SQS.
Now let’s talk about Actions and how these work together with whitelisting and blacklisting.
As you can see in the image above we have two options:
- Alert and Notify Me
- Block and Notify Me
If you select “Alert” the AWS Lambda execution will continue to run, however as the name and description imply a message will be sent to your team letting them know what took place.
The other option of “Block” will actually stop the operation from taking place. This means that your AWS Lambda execution will error. If you’re working on highly critical applications where you need high levels of security and this has been wide spread through your company and development team then this option may be worth it.
For example, imagine that your application was handling PCI data. In this circumstance, you would want to have as strong of restrictions as possible. When we get into multiple AWS accounts per environment/stage (e.g. dev, qa, prod), we may want to lean on blocking in a lower environment and then alert in production.
Blocking in a lower environment with proper testing before environment promotion should catch these issues early on. However, if you’re not confident in your team's ability to test effectively or want to air on the side of caution you could block these requests in production as well.
This is another situation where it depends on your company's context and where you are at with your serverless security journey.
In this article, we went through some security practices around the core serverless security services such as AWS API Gateway, AWS Lambda, and AWS DynamoDB. We talked through different practical scenarios and finally jumped into how to secure our serverless applications further with Thundra whitelisting and blacklisting.