Implementation of an application in a serverless model is associated with the Shared Responsibility Model - we share responsibility for the app’s security with the cloud service provider.
Amazon Web Services must take responsibility for securing the entire computing and network infrastructure, and the proper configuration of operating systems, installing security updates and patches on an ongoing basis, as well as encrypting data and network traffic. It’s up to the client, who’s a direct user of AWS services to implement its program in such a way as to prevent potential attackers from accessing confidential resources or performing undesirable actions.
If our code isn’t written in compliance with all security rules and hasn’t passed the appropriate tests, the application may be vulnerable to traditional application attacks, such as Cross-Site Scripting (XSS), Command/ SQL Injection, Denial of Service (DoS), broken authentication and authorization and more. How do we ensure the security of serverless applications in AWS?
Nowadays, every business application should have an access control mechanism to make sure that the user is really who they claim to be and only has access to the resources to which they are authorized.
The most common authentication method is a static password, which is usually stored in the database as hashed text. One of the known methods is hashing with cryptographic hash functions like MD5 and SHA1, which have long ceased to be considered secure. One of the newer algorithms is Bcrypt or PBKDF2. They’re equipped with built-in key stretching and salt addition mechanisms, which makes them highly resistant to brute force attacks, as well as those using rainbow tables. It’s also possible to authenticate users without the need to store passwords in the database or transfer them between the client and the server using the SRP protocol - Secure Remote Password.
At AWS, with serverless technology, we no longer have to think about the security of our credentials thanks to the Amazon Cognito service. Amazon also provides libraries for many programming languages to facilitate the implementation of the authentication logic, both with and without the use of SRP. At Cognito we can create our own user group or integrate with external identity providers such as Google, Facebook and others who support the OpenID Connect or SAML protocol - Security Assertion Markup Language. Such activities allow us to implement one of the best security practices - centralization of identity management, which translates into a reduction in the number of possible attack vectors.
From the application level, there’s a single identity database, where in fact user data may be stored in multiple places. AWS Cognito is a serverless security element.
For the security of serverless apps, Lambda function is also important. The shared responsibility model, mentioned above, according to which the client, i.e. the hired developers, care about the quality of the code being executed and its vulnerability to attacks. One of the serverless vulnerabilities included in OWASP Serverless Top 10 is Injection – by IBM’s definition: „a type of attack that allows an attacker to inject code into a program or query or inject malware onto a computer in order to execute remote commands that can read or modify a database, or change data on a web site”.
Lambda functions are often run as a response to the arrival of a specific event from another AWS service such as modifying the DynamoDB table or creating a new file in the S3 bucket. Be aware that the event object that stores information about such an event is available in the main Lambda method and in some cases may contain code injected by the attacker. Therefore, it’s important to remember that each portion of the input data that goes to the function is properly filtered before it’s used in a database query or in a shell command.
What is the danger behind this? In case of using defective code in a shell command, it can be used by a vulnerability called RCE - Remote Code Execution. An attacker can use this to take control of the runtime environment and thus other function calls, which will give him the ability to gain access to confidential information stored, for instance in a database, in the /tmp directory, and destroy selected application resources.
Apart from filtering and validating the input data, another important security practice of AWS Lambda is the application of the principle of least privilege. For this purpose, two assumptions must be met:
- Each Lambda in our application should be assigned a separate IAM (Identity and Access Management) role. It’s unacceptable to create one common role for all functions.
- The role of each Lambda should only allow operations that are performed. Avoid using a wildcard character (*) in your policies.
REST API access control
The serverless app can use the REST API to exchange data between components. In the AWS cloud, there’s a dedicated service called Amazon API Gateway, which is used to create and manage API endpoints. Despite the shared responsibility with the supplier, it should be remembered that the customer is responsible for the proper configuration. One of its key elements is the authorizer - mechanism built into API Gateway, whose task is to control access to the API.
First, consider a fragment of the application architecture using the Amazon API Gateway:
The client application sends requests to one or more REST API endpoints provided through the Amazon API Gateway service, which in turn calls the appropriate Lambda functions. If the application has an authentication mechanism, it uses the previously described Amazon Cognito service, which in turn uses JWT (JSON Web Tokens) to authenticate users and grant them access to resources.
JWT token consists of three parts: header, payload and signature, separated by a period. The Amazon Cognito service, if the user is correctly authenticated, returns three tokens to the application:
- ID token
- Access token
- Refresh token
The ID token and the Access token contain the credentials of the user's identity. They’re valid for one hour from the moment they’re generated. Refresh token allows you to regenerate the ID and Access token after their expiry date without the need to re-authenticate the user. Two RSA cryptographic key pairs are generated for each Cognito pool. One of the private keys is used to create the digital signature of the token.
Knowing the public key, both the client application and API Gateway, can easily verify whether the user using the JWT actually comes from our database and whether he’s not trying to impersonate another user. Any attempt to interfere with the string creating the token will make the signature invalid.
Let us now examine the modified application architecture diagram above:
The request to the REST API sent by the client application includes an additional HTTP header called Authorization. The value of this header can be an ID token or an Access token previously sent to the application in response to the user's successful authentication. The API Gateway service, before forwarding the request to the Lambda function, makes sure that the token has actually been signed with the key associated with the correct Cognito pool and that the token's expiration date hasn’t expired.
If verification fails, the request will be rejected. In this situation, even if the potential attacker guesses the endpoint URL, he won’t be able to successfully send any request to the API without being a registered user of the specified application.
Another available type of authorizer is the Lambda authorizer, which is a replacement for the Cognito authorizer. This solution uses the Lambda function that’s triggered when the request arrives at the endpoint. The function contains its own verification logic for the request, based on the token provided or based on the headers and parameters sent with the request. As a result of Lambda's operation, an object containing the IAM policy must be returned, which determines whether API Gateway should accept or reject the request.
Security of access data
During use of the Amazon DynamoDB database, we don’t have to worry about the process of establishing a connection and authenticating with a username and password, which is usually the case with typical database servers. Communication takes place via the http/s protocol and each sent request contains a cryptographic signature. The developer usually doesn’t need to know the low-level details of how this interface works, as they can use a convenient, high-level API using AWS CLI or AWS SDK packages.
However, in some applications, there’s a need to use a different database, which is associated with storing access data in the next layer of the application. A good solution may be to use Lambda's encrypted environment variables and even better - using AWS Secrets Manager. This service allows you to safely store passwords, API access keys and other confidential information. The developer can easily download such data to the Lambda function code by calling the appropriate method from the AWS SDK package.
If our app uses a database server or cluster created with the Amazon RDS service, we can use IAM authentication. After properly configuring the database and the IAM role assigned to the Lambda function, one calling the method from the AWS SDK retrieves a temporary access token, which we then use instead of the password in the standard procedure of establishing a connection with the database. It’s required that it’s an encrypted SSL connection - Secure Sockets Layer, which additionally increases the security level of the solution.
The serverless model doesn’t eliminate the need to deal with the issue of security. The above-mentioned tools and services available in the AWS cloud certainly facilitate this task. However, I’d like to point out that these are only the basic possible elements supporting the security of our serverless applications.