A few weeks ago, the Linux Foundation and partners announced the sigstore project, a non-profit software signing service. The recent Solarwind security issues have shown that it is vital to protect your software supply chain from unauthorized changes. As security is an important part of any application, developers and organisations should follow the guidelines of the Secure Software Development Lifecycle (SSDLC). The Solarwinds hacks have renewed interest in SSLDC and the software development pipeline.
In a nutshell, the SSDCL has 5 phases:
- Requirements
- Design
- Development
- Verification / Testing
- Deployment
In this blog post, we’ll look into AWS Signer and how it can assist with Phase 5 - Deployment, in a serverless environment with AWS Lambda.
AWS Signer & Code Signing
> AWS Signer is a fully managed code-signing service to ensure the trust and integrity of your code. Organizations validate code against a digital signature to confirm that the code is unaltered and from a trusted publisher. source
So what is Code signing? Code signing is the process of applying a digital signature to software artifacts. This provides a level of trust that the code has not been tampered with since being published. Code signing helps prove:
- Content source: Code signing identifies that the software or application is coming from a specific source, in this case AWS Signer.
- Content integrity: Code signing ensures that the code has not been altered or tampered with.
- The Best thing about AWS Signer? It’s free.
Code Signing Configuration for AWS Lambda
> A code signing configuration defines a list of allowed signing profiles and defines the code-signing validation policy. source
The creation of a code signing configuration is straightforward, with just 2 properties deserving some extra attention:
- AllowedPublishers: Signing profiles that can use the code signing configuration.
- UntrustedArtifactOnDeployment: This can either be Enforce or Warn. If you set the policy to Enforce, Lambda will block the deployment if the signature validation check fails. When set to Warn, Lambda allows the deployment and creates a Cloudwatch log (viewable in Cloudwatch Metrics).
Source code
You can find all the source code for the examples in this blogpost in this Github repository.
The following tools are used:
- AWS CDK: The CDK is used to quickly bootstrap some infrastructure.
- AWS CLI: We use the AWS CLI to start signing jobs, create functions and deploy code. This is something that should be automated in a pipeline, but is simplified for this post.
- Nodejs: since the CDK application is written in Typescript
Your local environment should be configured to deploy the infrastructure generated by the CDK and run commands on the AWS CLI.
You’re probably wondering why we aren’t creating the lambda function as well using the CDK For the demo, we’ll configure the code signing config on Enforce. This means that creating an AWS Lambda without a signed code package, will fail. This feature is currently not supported out of the box in the CDK, so we’ll have to manage the resource using the AWS CLI. If we would have configured the Code Signing Config with Warn, then we could have used the CDK to deploy a lambda.
What do we need?
- Versioned S3 Bucket: AWS Signer stores its signed artifacts in a **versioned** S3 bucket.
- Signer profile: AWS Signer profile that is going to be used to sign software artifacts
- Code signing config: This code signing config uses the AWS Signer profile and will be linked to a Lambda function.
- Credentials to execute code signing from your terminal.
Code signing and validation in action
With the infrastructure in place, we’ll start with creating a Lambda function and attach the code signing config that we’ve created with the CDK. Make sure you’ve uploaded some sample code in the versioned bucket, as we will deploy this later. You can find a zip file in the example repo in the folder `function`.
Now, let’s have a look at the IAM statements to deploy the lambda. The code signing configuration supports `Condition`[Condition](https://docs.aws.amazon.com/service-authorization/latest/reference/list_awslambda.html) in IAM policies. This can be leveraged to force new lambdas to be created and updated using the code signing configuration.
{
"Condition": {
"StringEquals": {
"lambda:CodeSigningConfigArn": [
"arn:aws:lambda:<region>:<acount-id>:code-signing-config:csc-<csc-id>"
]
}
},
"Action": [
"lambda:CreateFunction",
"lambda:PutFunctionCodeSigningConfig"
],
"Resource": "*",
"Effect": "Allow"
}
The policy above will block all attempts to create a Lambda function without the Code Signing Configuration specified in the policy. Let’s try and create the Lambda:
aws lambda create-function \
--function-name "code-signed-function" \
--runtime "nodejs14.x" \
--role <lambda-role> \
--code S3Bucket=<bucketname>,S3Key=<zip key>, S3ObjectVersion=<version> \
--handler index.handler \
--code-signing-config-arn <code-signing-config-arn>
When executing the above command, we have as expected an error.
An error occurred (CodeVerificationFailedException) when calling the CreateFunction operation: Lambda cannot deploy the function. The function or layer might be signed using a signature that the client is not configured to accept. Check the provided signature for code-signed-function
This means that we need to sign our code first.
Let’s have a quick look at the minimum IAM policies to start a code signing job.
{
"Action": [
"s3:GetObjectVersion",
"s3:PutObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::<bucket-name>/*",
"arn:aws:s3:::<bucket-name>"
],
"Effect": "Allow"
},
{
"Action": [
"signer:GetSigningProfile",
"signer:StartSigningJob"
],
"Resource": "arn:aws:signer:<region>:<account-id>:/signing-profiles/<code-signing-profile-id>",
"Effect": "Allow"
}
When configured correctly, we now have permissions to start the code signing job. We’ll tell AWS Signer to put all our signed artifacts in the `/signed` folder in the S3 bucket.
Starting a job using the AWS CLI is rather easy and straightforward and can be done with the following command:
aws signer start-signing-job \
--source 's3={bucketName=<lambda-bucket>, version=<version-string>, key=<zip-key>}' \
--destination 's3={bucketName=<lambda-bucket>, prefix=signed/}' \
--profile-name <aws-signer-profile>
When the signing job is complete, we can retry to create the function, using the signed zip code. This will create the lambda, with the specific code signing config. So now we are sure that our lambda will require a code signed deployment package.
But what about updates? Trying to update a function without a code signed deployment package will throw the following error:
An error occurred (CodeVerificationFailedException) when calling the UpdateFunctionCode operation: Lambda cannot deploy the function. The function or layer might be signed using a signature that the client is not configured to accept. Check the provided signature
So the same workflow as for creating a lambda function is required when updating your function code. This ensures that all your deployed code artifacts are signed.
Recap
With the correct IAM statements Lambda creation can only pass when created with a specific code signing configuration
The code signing configuration can enforce that all Lambda deployment artifacts are code signed using AWS Signer.
The example in this post showed how you can secure your serverless applications using AWS Signer as a managed code-signing service combined with code signing configuration for AWS Lambda. Both the creation of Lambda functions and its updates are having their code signature verified.
This recent event is an example of an intended malicious code injection in a NPM package so keep in mind that code signing alone does not protect against all malicious attacks and signing your code is just 1 step in the SSDLC process. So proper validation of the code and its dependencies is still required before it gets signed.
Interested in working together?
Get in touch