Security Best Practices - Serverless Architectures with AWS Lambda

Security Best Practices

Designing and implementing security into your applications should always be priority number one—this doesn’t change with a serverless architecture. The major difference for securing a serverless application compared to a server-hosted application is obvious—there is no server for you to secure. However, you still need to think about your application’s security. There is still a shared responsibility model for serverless security.

With Lambda and serverless architectures, rather than implementing application security through things like antivirus/malware software, file integrity monitoring, intrusion detection/prevention systems, firewalls, etc., you ensure security best practices through writing secure application code, tight access control over source code changes, and following AWS security best practices for each of the services that your Lambda functions integrate with.

The following is a brief list of serverless security best practices that should apply to many serverless use cases, although your own specific security and compliance requirements should be well understood and might include more than we describe here.

  • One IAM Role per Function

    Each and every Lambda function within your AWS account should have a 1:1 relationship with an IAM role. Even if multiple functions begin with exactly the same policy, always decouple your IAM roles so that you can ensure least privilege policies for the future of your function.

    For example, if you shared the IAM role of a Lambda function that needed access to an AWS KMS key across multiple Lambda functions, then all of those functions would now have access to the same encryption key.

  • Temporary AWS Credentials

    You should not have any long-lived AWS credentials included within your Lambda function code or configuration. (This is a great use for static code analysis tools to ensure it never occurs in your code base!) For most cases, the IAM execution role is all that’s required to integrate with other AWS services. Simply create AWS service clients within your code through the AWS SDK without providing any credentials. The SDK automatically manages the retrieval and rotation of the temporary credentials generated for your role. The following is an example using Java.

AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient(); Table myTable = new Table(client, "MyTable");

This code snippet is all that’s required for the AWS SDK for Java to create an object for interacting with a DynamoDB table that automatically sign its requests to the DynamoDB APIs, using the temporary IAM credentials assigned to your function.

However, there might be cases where the execution role is not sufficient for the type of access your function requires. This can be the case for some cross-account integrations your Lambda function might perform, or if you have user-specific access control policies through combining Amazon Cognito identity roles and DynamoDB fine-grained access control. For cross-account use cases, you should grant your execution role should be granted access to the AssumeRole API within the AWS Security Token Service and integrated to retrieve temporary access credentials.

For user-specific access control policies, your function should be provided with the user identity in question and then integrated with the Amazon Cognito API GetCredentialsForIdentity. In this case, it’s imperative that you ensure your code appropriately manages these credentials so that you are leveraging the correct credentials for each user associated with that invocation of your Lambda function. It’s common for an application to encrypt and store these per-user credentials in a place like DynamoDB or Amazon ElastiCache as part of user session data, so that they can be retrieved with reduced latency and more scalability than regenerating them for subsequent requests for a returning user.

  • Persisting Secrets

    There are cases where you may have long-lived secrets (for example, database credentials, dependency service access keys, encryption keys, etc.) that your Lambda function needs to use. We recommend a few options for the lifecycle of secrets management in your application:

  • Lambda Environment Variables with Encryption Helpers

    Advantages – Provided directly to your function runtime environment, minimizing the latency and code required to retrieve the secret.

    Disadvantages – Environment variables are coupled to a function version. Updating an environment variable requires a new function version (more rigid, but does provide stable version history as well).

  • Amazon EC2 Systems Manager Parameter Store

    Advantages – Fully decoupled from your Lambda functions to provide maximum flexibility for how secrets and functions relate to each other.

    Disadvantages – A request to Parameter Store is required to retrieve a parameter/secret. While not substantial, this does add latency over environment variables as well as an additional service dependency, and requires writing slightly more code.

  • Using Secrets

    Secrets should always only exist in memory and never be logged or written to disk. Write code that manages the rotation of secrets in the event a secret needs to be revoked while your application remains running.

  • API Authorization

    Using API Gateway as the event source for your Lambda function is unique from the other AWS service event source options in that you have ownership of authentication and authorization of your API clients. API Gateway can perform much of the heavy lifting by providing things like native AWS SigV4 authentication, generated client SDKs, and custom authorizers. However, you’re still responsible for ensuring that the security posture of your APIs meets the bar you’ve set. For more information about API security best practices, see this documentation.

  • VPC Security

    If your Lambda function requires access to resources deployed inside a VPC, you should apply network security best practices through use of least privilege security groups, Lambda function-specific subnets, network ACLs, and route tables that allow traffic coming only from your Lambda functions to reach intended destinations.

    Keep in mind that these practices and policies impact the way that your Lambda functions connect to their dependencies. Invoking a Lambda function still occurs through event sources and the Invoke API (neither are affected by your VPC configuration).

  • Deployment Access Control

    A call to the UpdateFunctionCode API is analogous to a code deployment. Moving an alias through the UpdateAlias API to that newly published version is analogous to a code release. Treat access to the Lambda APIs that enable function code/aliases with extreme sensitivity. As such, you should eliminate direct user access to these APIs for any functions (production functions at a minimum) to remove the possibility of human error. Making code changes to a Lambda function should be achieved through automation. With that in mind, the entry point for a deployment to Lambda becomes the place where your continuous integration/continuous delivery (CI/CD) pipeline is initiated. This may be a release branch in a repository, an S3 bucket where a new code package is uploaded that triggers an AWS CodePipeline pipeline, or somewhere else that’s specific to your organization and processes. Wherever it is, it becomes a new place where you should enforce stringent access control mechanisms that fit your team structure and roles.