Scan packages for vulnerabilities – Serverless and Security

Scan packages for vulnerabilities

You should also run continuous vulnerability scans in response to new packages, package upgrades, and reports of new vulnerabilities. Scans can be run against a code repository using tools such as Snyk or GitHub’s native Dependabot alerts system.

Automate dependency upgrades

Out of all the suggestions for securing your supply chain, this is the most crucial. Even if you have a serverless application with copious packages distributed across multiple services, make sure upgrades of all dependencies are automated.

Warning

While automating upgrades of your application’s dependencies is generally a recommended practice, you should always keep in mind the “think before you install” checklist from the previous section. You should be particularly mindful of the integrity of the incoming updates, in case a bad actor has published a malicious version of a package.

Keeping package versions up-to-date ensures that you not only have access to the latest features but, crucially, to the latest security patches. Vulnerabilities can be found in earlier versions of software after many later versions have been published. Navigating an upgrade across several minor versions can be difficult enough, depending on the features of the package, the adherence to semantic versioning by the authors, and the prevalence of the package throughout your codebase—but upgrading from one major version to another is typically not trivial, given the likelihood of the next version containing breaking changes that affect your usage of the package.

Runtime updates

As well as dependency upgrades, it is highly recommended to keep up-to-date with the latest version of the AWS Lambda runtime you are using. Make sure you are subscribed to news about runtime support and upgrade as soon as possible.

Warning

By default, AWS will automatically update the runtime of your Lambda functions with any patch versions that are released. Additionally, you have the option to control when the runtime of your functions is updated through Lambda’s runtime management controls.

These controls are primarily useful for mitigating the rare occurrence of bugs caused by a runtime patch version that is incompatible with your function’s code. But, as these patch versions will likely include security updates, you should use these controls with caution. It is usually safest to keep your functions running on the latest version of the runtime.

The same is true for any delivery pipelines you maintain, as these will likely run on virtual machines and runtimes provided by the third party. And remember, you do not need to use the same runtime version across pipelines and functions. For example, you should use the latest version of Node.js in your pipelines even before it is supported by the Lambda runtime.

Going Further with SLSA – Serverless and Security

Going Further with SLSA

The SLSA security framework (pronounced salsa, short for Supply chain Levels for Software Artifacts) is “a checklist of standards and controls to prevent tampering, improve integrity, and secure packages and infrastructure.” SLSA is all about going from “safe enough” to maximum resiliency across the entire software supply chain.

If you’re at a fairly high level of security maturity, you may find it useful to use this framework to measure and improve the security of your software supply chain. Follow the SLSA documentation to get started. The Software Component Verification Standard (SCVS) from OWASP is another framework for measuring supply chain security.

Lambda Code Signing

The last mile in the software supply chain is packaging and deploying your function code to the cloud. At this point, your function will usually consist of your business logic (code you have authored) and any third-party libraries listed in the function’s dependencies (code someone else has authored).

Lambda provides the option to sign your code before deploying it. This enables the Lambda service to verify that a trusted source has initiated the deployment and that the code has not been altered or tampered with in any way. Lambda will run several validation checks to verify the integrity of the code, including that the package has not been modified since it was signed and that the signature itself is valid.

To sign your code, you first create one or more signing profiles. These profiles might map to the environments and accounts your application uses—for example, you may have a signing profile per AWS account. Alternatively, you could opt to have a signing profile per function for greater isolation and security. The CloudFormation resource for a signing profile looks like this, where the PlatformID denotes the signature format and signing algorithm that will be used by the profile:


{
“Type”
:
“AWS::Signer::SigningProfile”
,
“Properties”
:
{
“PlatformId”
:
“AWSLambda-SHA384-ECDSA”
,
}
}

Once you have defined a signing profile, you can then use it to configure code signing for your functions:


{
“Type”
:
“AWS::Lambda::CodeSigningConfig”
,
“Properties”
:
{
“AllowedPublishers”
:
[
{
“SigningProfileVersionArns”
:
[
“arn:aws:signer:us-east-1:123456789123:/signing-profiles/my-profile”
]
}
],
“CodeSigningPolicies”
:
{
“UntrustedArtifactOnDeployment”
:
“Enforce”
}
}
}

Finally, assign the code signing configuration to your function:


{
“Type”
:
“AWS::Lambda::Function”
,
“Properties”
:
{
“CodeSigningConfigArn”
:
[
“arn:aws:lambda:us-east-1:123456789123:code-signing-config:csc-config-id”
,
]
}
}

Now, when you deploy this function the Lambda service will verify the code was signed by a trusted source and has not been tampered with since being signed.

Protecting Serverless APIs – Serverless and Security

Protecting Serverless APIs

According to the OWASP Top 10 list we looked at earlier in this chapter, the number one threat to web applications is broken access control. While serverless helps to mitigate some of the threats posed by broken access control, you still have work to do in this area.

When applying the zero trust security model, you must apply access control to each isolated component as well as the perimeter of your system. For most serverless applications the security perimeter will be an API Gateway endpoint. If you are building a serverless application that exposes an API to the public internet, you must design and implement an appropriate access control mechanism for this API.

In this section, we’ll explore the available authorization strategies for applying access control to serverless APIs and when to use each one. The access control options for API Gateway are summarized in Table 4-2.

Note

Amazon API Gateway provides two types of APIs: REST APIs and HTTP APIs. They offer different features at different costs. One of the differences is the access control options available. The compatibility for each of the access control methods we’ll explore in this section is indicated in Table 4-2.

Table 4-2. Amazon API Gateway access control options Access control strategyDescriptionREST APIHTTP API
Cognito authorizersDirect integration with the access management service Amazon Cognito and API Gateway REST APIs. Cognito client credentials are exchanged for access tokens, which are validated directly with Cognito.YesNo
JWT authorizersCan be used to integrate an access management service that uses JSON Web Tokens (JWTs) for access control, such as Amazon Cognito or Okta, with API Gateway HTTP APIs.NoaYes
Lambda authorizersLambda functions can be used to implement custom authorization logic when using an access management service other than Cognito or to verify incoming webhook messages where user-based authentication is not available.YesYes
a You can still use JWTs to authorize and authenticate REST API requests, but you will need to write a custom Lambda authorizer that verifies incoming tokens.

Securing REST APIs with Amazon Cognito – Serverless and Security

Securing REST APIs with Amazon Cognito

There are of course many access management services and identity providers available, including Okta and Auth0. We’ll focus on using Cognito to secure a REST API, as it is native to AWS and for this reason provides minimal overhead.

Amazon Cognito

Before we dive in, let’s define the building blocks you will need. Cognito is often viewed as one of the most complex AWS services. It is therefore important to have a foundational understanding of Cognito’s components and a clear idea of the access control architecture you are aiming for. Here are the key components for implementing access control using Cognito:

User pools

A user pool is a user directory in Amazon Cognito. Typically you will have a single user pool in your application. This user pool can be used to manage all the users of your application, whether you have a single user or multiple users.

Application clients

You may be building a traditional client/server web application where you maintain a frontend web application and a backend API. Or you may be operating a multitenant business-to-business platform, where tenant backend services use a client credentials grant to access your services. In this case, you can create an application client for each tenant and share the client ID and secret with the tenant backend service for machine-to-machine authentication.

Scopes

Scopes are used to control an application client’s access to specific resources in your application’s API.

Application-Based Multitenancy

Multitenancy is an identity and access management architecture to support sharing the underlying resources of an application between multiple groups of users, or tenants. Conversely, in a single-tenant architecture each tenant is assigned to a separate instance of the application running on distinct infrastructure. Although tenants share the same infrastructure in multitenancy, their data is completely separated and never shared between tenants.

Multitenancy is a very common model that is often used in SaaS products and by cloud vendors themselves, including AWS. Multitenancy is also complementary to a zero trust architecture, where a more granular access control model is required. Consider a single consumer of your API: it is likely the consumer will have multiple services of its own consuming multiple API resources. In this scenario, a secure, zero trust approach would be to isolate usage of the API to consuming services, granting only the minimum permissions needed by that individual service to perform its requests of the API.

Application-based multitenancy is a technique that will allow your application’s access control to scale, regardless of whether you start with a single user or multiple tenants. Each tenant of your application is assigned at least one application client and a set of scopes that determine the resources the app client can access. If you never scale beyond a single user, adopting this architecture will still serve you well. Take a look at the Cognito documentation for more information about implementing app client–based multitenancy.

Cognito and API Gateway

Cognito authorizers provide a fully managed access control integration with API Gateway, as illustrated in Figure 4-5. API consumers exchange their credentials (a client ID and secret) for access tokens via a Cognito endpoint. These access tokens are then included with API requests and validated via the Cognito authorizer.

Figure 4-5. API Gateway Cognito authorizer

Additionally, an API endpoint can be assigned a scope. When authorizing a request to the endpoint, the Cognito authorizer will verify the endpoint’s scope is included in the client’s list of permitted scopes.

Securing HTTP APIs – Serverless and Security

Securing HTTP APIs

If you are using an API Gateway HTTP API, rather than a REST API, you will not be able to use the native Cognito authorizer. Instead, you have a few alternative options. We’ll explore examples of the most convenient two: Lambda authorizers and JWT authorizers.

Tip

JWT authorizers can also be used to authenticate API requests with Amazon Cognito when using HTTP APIs.

JWT authorizers

If your authorization strategy simply involves a client submitting a JSON Web Token for verification, using a JWT authorizer will be a good option. When you use a JWT authorizer, the whole authorization process is managed by the API Gateway service.

Note

JWT is an open standard that defines a compact, self-contained way of securely transmitting information between parties as JSON objects. JWTs can be used to ensure the integrity of a message and the authentication of both the message producer and consumer.

JWTs can be cryptographically signed and encrypted, enabling verification of the integrity of the claims contained within the token while keeping those claims hidden from other parties.

You first configure the JWT authorizer and then attach it to a route. The Cloud­For⁠mation resource will look something like this:


{
“Type”
:
“AWS::ApiGatewayV2::Authorizer”
,
“Properties”
:
{
“ApiId”
:
“ApiGatewayId”
,
“AuthorizerType”
:
“JWT”
,
“IdentitySource”
:
[
“$request.header.Authorization”
],
“JwtConfiguration”
:
{
“Audience”
:
[
“https://my-application.com”
],
“Issuer”
:
“https://cognito-idp.us-east-1.amazonaws.com/userPoolID”
},
“Name”
:
“my-authorizer”
}

}

The IdentitySource should match the location of the JWT provided by the client in the API request; for example, the Authorization HTTP header. The Jwt​Config⁠ura⁠tion should correspond to the expected values in the tokens that will be submitted by clients, where the Audience is the HTTP address for the recipient of the token (usually your API Gateway domain) and the Issuer is the HTTP address for the service responsible for issuing tokens, such as Cognito or Okta.

Lambda authorizers – Serverless and Security

Lambda authorizers

Lambda functions with custom authorization logic can be attached to API Gateway HTTP API routes and invoked whenever requests are made. These functions are known as Lambda authorizers and can be used when you need to apply access control strategies beyond the ones the managed Cognito or JWT authorizers support. The functions’ responses will either approve or deny access to the requested resources (see Figure 4-6).

Figure 4-6. Controlling access to API Gateway resources with a Lambda authorizer

Lambda authorizers support various locations for providing authorization claims in API requests. These are known as identity sources and include HTTP headers and query string parameters (for example, the Authorization header). The identity source you use will be required in requests made to API Gateway; any requests without the required property will immediately receive a 401 Unauthorized response and the Lambda authorizer will not be invoked.

Lambda authorizer responses can also be cached. The responses will be cached according to the identity source provided by the API’s clients. If a client provides the same values for the required identity sources within the configured cache period, or TTL, API Gateway uses the cached authorizer result instead of invoking the authorizer function.

Tip

Caching the responses of your Lambda authorizers will result in quicker responses to API requests as well as a reduction in costs, as the Lambda function will be invoked significantly less frequently.

The Lambda function used to authorize requests can return an IAM policy or what is known as a simple response. The simple response will usually suffice, unless your use case requires an IAM policy response or more granular permissions. When using the simple response, the authorizer function must return a response matching the following format, where isAuthorized is a Boolean value that denotes the outcome of your authorization checks and context is optional and can include any additional information to pass to API access logs and Lambda functions integrated with the API resource:


{
“isAuthorized”
:
true
/
false
,
“context”
:
{
“key”
:
“value”
}
}

Validating and Verifying API Requests – Serverless and Security

Validating and Verifying API Requests

There are other ways to protect your serverless API beyond the access control mechanisms we have explored so far in this section. In particular, publicly accessible APIs should always be protected against deliberate or unintentional misuse and incoming request data to those APIs should always be validated and sanitized.

API Gateway request protection

API Gateway offers two ways of protecting against denial of service and denial of wallet attacks.

First, requests from individual API clients can be throttled via API Gateway usage plans. Usage plans can be used to control access to API stages and methods and to limit the rate of requests made to those methods. By rate limiting API requests, you can prevent any of your API’s clients from deliberately or inadvertently abusing your service. Usage plans can be applied to all methods in an API, or to specific methods. Clients are given a generated API key to include in every request to your API. If a client submits too many requests and is throttled as a result, they will begin to receive 429 Too Many Requests HTTP error responses.

API Gateway also integrates with AWS WAF to provide granular protection at the request level. With WAF, you can specify a set of rules to apply to each incoming request, such as IP address throttling.

Note

WAF rules are always applied before any other access control mechanisms, such as Cognito authorizers or Lambda authorizers.

API Gateway request validation

Requests to API Gateway methods can be validated before being processed further. Say you have a Lambda function attached to an API route that accepts the API request as an input and applies some operations to the request body. You can supply a JSON Schema definition of the expected input structure and format, and API Gateway will apply those data validation rules to the body of a request before invoking the function. If the request fails validation, the function will not be invoked and the client will receive a 400 Bad Request HTTP response.

Note

Implementing request validation via API Gateway can be particularly useful when using direct integrations to AWS services other than Lambda. For example, you may have an API Gateway resource that integrates directly with Amazon EventBridge, responding to API requests by putting events onto an event bus. In this architecture you will always want to validate and sanitize the request payload before forwarding it to downstream consumers.

For more information about functionless integration patterns, refer to Chapter 5.

In the following example JSON model, the message property is required, and the request will be rejected if that field is missing from the request body:


{
“$schema”
:
“http://json-schema.org/draft-07/schema#”
,
“title”
:
“my-request-model”
,
“type”
:
“object”
,
“properties”
:
{
“message”
:
{
“type”
:
“string”
},
“status”
:
{
“type”
:
“string”
}
},
“required”
:
[
“message”
]
}

Deeper input validation and sanitization should be performed in Lambda functions where data is transformed, stored in a database or delivered to an event bus or message queue. This can secure your application from SQL injection attacks, the #3 threat in the OWASP Top 10 (see Table 4-1).

Message Verification in Event-Driven Architectures – Serverless and Security

Message Verification in Event-Driven Architectures

Most of the access control techniques we’ve explored generally apply to synchronous, request/response APIs. But as you learned in Chapter 3, it is highly likely that, as you and the teams or third parties you interact with are building event-driven applications, at some point you will encounter an asynchronous API.

Message verification is typically required at the integration points between systems—for example, of incoming messages from third-party webhooks and messages sent by your application to other systems or accounts. In a zero trust architecture message verification is also important for messaging between services in your application.

Verifying messages between consumers and producers

Typically, in order to decouple services, the producer of an event is deliberately unaware of any downstream consumers of the event. For example, you might have an organization-wide event backbone architecture where multiple producers send events to a central event broker and multiple consumers subscribe to these events, as shown in Figure 4-7.

Figure 4-7. Decoupled producers and consumers in an event-driven architecture

Securing consumers of asynchronous APIs relies on the control of incoming messages. Consumers should always be in control of the subscription to an asynchronous API, and there will already be a certain level of trust established between the event producers and the event broker—but event consumers must also guard against sender spoofing and message tampering. Verification of incoming messages is crucial in event-driven systems.

Let’s assume the event broker in Figure 4-7 is an Amazon EventBridge event bus in a central account, part of your organization’s core domain. The producers are services deployed to separate AWS accounts, and so are the consumers. A consumer needs to ensure each message has come from a trusted source. A producer needs to ensure messages can only be read by permitted consumers. For a truly decoupled architecture, the event broker itself might be responsible for message encryption and key management (rather than the producer), but for the purpose of keeping the example succinct we’ll make this the producer’s responsibility.

Built-in message verification for SNS – Serverless and Security

Built-in message verification for SNS

In addition to the approach outlined in the previous section, some AWS services, such as Amazon Simple Notification Service (SNS), are now beginning to support message signatures natively. SNS signs the messages delivered from your topic, enabling the subscribed HTTP endpoints to verify their authenticity.

Protecting Data

Data is the most valuable asset accumulated by any software application. This includes data personal to users of the application, data about third-party integrations with the application, and data about the application itself.

Cryptographic failure is the second of the OWASP Top 10 threats to web applications, after broken access control. This section examines the crucial role of data encryption in securing a serverless application and how you can encrypt your data as it moves through your system.

Data Encryption Everywhere

As you develop and operate your serverless application, you will discover both the power and the challenges that come with connecting components with events. Events allow you to decouple components and include rich data in their messages. Serverless compute is inherently stateless, which means the data a Lambda function or Step Functions workflow needs to perform its operations must either be queried from a data store, like DynamoDB or S3, or provided in the invocation payload.

In event-driven systems, data is everywhere. This means data encryption needs to be everywhere too. Data will be stored in databases and object stores. It will be moved through message queues and event buses. Dr. Werner Vogels, the CTO and VP of Amazon, once said on stage at re:Invent, “Dance like no one is watching. Encrypt like everyone is.”

What is encryption?

Encryption is a technique for restricting access to data by making it unreadable without a key. Cryptographic algorithms are used to obscure plain-text data with an encryption key. The encrypted data can only be decrypted with the same key.

Encryption is your primary tool in protecting the data in your application. It’s particularly important in event-driven applications, where data constantly flows between accounts, services, functions, data stores, buses, and queues. Encryption can be divided into two categories: encryption at rest and encryption in transit. By encrypting data both in transit and at rest, you ensure that your data is protected for its entire lifecycle and end-to-end as it passes through your system and into other systems.

Most AWS managed services offer native support for encryption as well as direct integration with AWS Secrets Manager and AWS KMS. This means the process of encrypting and decrypting data and managing the associated encryption keys is largely abstracted away from you. However, encryption is not usually enabled by default, so you are responsible for enabling and configuring encryption at the resource level.

Encryption in transit – Serverless and Security

Encryption in transit

Data is in transit in a serverless application as it moves from service to service. All AWS services provide secure, encrypted HTTP endpoints via Transport Layer Security (TLS). Whenever you are interacting with the API of an AWS service, you should use the HTTPS endpoint. By default, operations you perform with the AWS SDK will use the HTTPS endpoints of all AWS services. For example, this means when your Lambda function is invoked by an API Gateway request and you make an EventBridge PutEvents call from the function, the payloads are entirely encrypted when in transit.

In addition to TLS, all AWS API requests made using the AWS SDK are protected by a request signing process, known as Signature Version 4. This process is designed to protect against request tampering and sender spoofing.

Encryption at rest

Encryption at rest is applied to data whenever it is stored or cached. In a serverless application, this could be data in an EventBridge event bus or archive, a message on an SQS queue, an object in an S3 bucket, or an item in a DynamoDB table.

As a general rule, whenever a managed service offers the option to encrypt data at rest you should take advantage of it. However, this is especially important when you have classified the data at rest as sensitive.

You should always limit the storage of data at rest and in transit. The more data is stored, and the longer it is stored for, the greater the attack surface area and security risk. Only store or transport data if it is absolutely necessary, and continually review your data models and event payloads to ensure redundant attributes are removed.

Tip

There are also sustainability benefits to storing less data. See Chapter 10 for more information on this topic.

AWS KMS

The key (pun intended) AWS service when it comes to encryption is AWS Key Management Service. AWS KMS is a fully managed service that supports the generation and management of the cryptographic keys that are used to protect your data. Whenever you use the native encryption controls of an AWS service like Amazon SQS or S3, as described in the previous sections, you will be using KMS to create and manage the necessary encryption keys. Whenever a service needs to encrypt or decrypt data, it will make a request to KMS to access the relevant keys. Access to keys is granted to these services via their attached IAM roles.

There are several types of KMS keys, such as HMAC keys and asymmetric keys, and these are generally grouped into two categories: AWS managed keys and customer managed keys. AWS managed keys are encryption keys that are created, managed, rotated, and used on your behalf by an AWS service that is integrated with AWS KMS. Customer managed keys are encryption keys that you create, own, and manage. For most use cases, you should choose AWS managed keys whenever available. Customer managed keys can be used in cases where you are required to audit usage of or retain additional control over the keys.

Note

The AWS documentation has a detailed explanation of KMS concepts if you’d like to read more about this topic.

Key and Secret Management Guardrails

As your application grows, it will utilize various cryptographic keys and secrets to handle the encryption and security of data. It is important to establish clear and actionable key management guardrails for your team. Here are some suggestions:

Use a secure vault.

Always use a secure vault to store encryption keys and secrets, like AWS KMS, Secrets Manager, or Systems Manager Parameter Store (with secure strings).

Never store secrets outside the secure vault.

For example, do not store keys on a local machine or in a code repository. Only fetch keys and secrets at runtime, and do not cache them.

Ensure keys are protected from unauthorized access.

For example, enforce least privilege in IAM policies that access KMS keys.

Use independent keys.

When multiple keys are required, use distinct keys rather than sharing keys across services or clients.

Manage the lifecycle of keys.

Devise a strategy for rotating keys (and algorithms) based on the type of key and its intended purpose. Key rotation can be either manual or automated.