AWS Code Signing with Terraform
By: Karl Cardenas
Tags: Ci Cd Pipeline, Aws Code Signing, Terraform, AWS, Code Signing

In this article there are three options for leveraging AWS Code Signing with Terraform. When you make the decision to leverage code signing with AWS Lambda understand that you are accepting the following requirements:

The Lambda binary/zip must be sourced from S3

Inline editing is no longer enabled

All Lambda binaries/zip files must be signed by AWS Signer service

An inactivated code signer profile will be deleted after two years

For most people these requirements are not an issue, if anything they promote immutability (this is a good thing) and automated code deployment practices. However, the requirements could potentially be a bit too restrictive for a low priority environments (sandbox, research, test) so just be aware of this.

Enable AWS Code Signing

When enabling code signing it is best practice to do so through infrastrcuture as code (IaC). This article will leverage Terraform as the chooses IaC solution.

Enabling AWS Code Signing through Terraform requires two unique resources to be created, aws_signer_signing_profile and aws_lambda_code_signing_config .

aws_signer_signing_profile creates an AWS Signer profile that will be used when creating a signing job (more on signing jobs later). The profile is the corner stone for all future steps. It’s also in the profile resource definition where you can specify the signature_validity_period . After a signing profile is created it needs to be referenced in a configuration. Without a profile signing configuration the AWS Signer profile is limited in usage. A code signing configuration dictates what code signing profiles to trust/allow and how to handle artifacts that lack a code signing signature.

Once you have a profile and a profile signing configuration created, we then reference the code signing configuration aws_lambda_code_signing_config in a Lambda. This is needed in order to leverage code signing for Lambda deployments. Let’s take a peek at how that is done.

The code below is in the context of a Lambda managed by Terraform. Observe line 5 as that is where the code signing configuration is referenced and enabled for this specific Lambda.

Before code signing, if we would had deployed this Lambda it would had looked like this from the AWS console.

Inline editing is allowed

But after enabling code signing for the Lambda, it now looks like this when visiting the AWS console.

Inline editing is disabled

As mentioned earlier, inline editing is now disabled, and because the AWS Code Signing configuration created had a policy that enforced code signing we are unable to upload zipped binaries that lack a code signing signature. So with that background out of the way, let’s dive into how one would integrate this into a Terraform configuration.

Option 1- 100% Terraform Managed

If you manage the Lambda and the binary creation process in the same Terraform template then this is the option for you.

The code below assumes that the AWS Signer Profile and AWS Signer Configuration has been created in the same project but you can easily modify this and leverage a data resource to reference the profile and code signing configuration.

Everything is managed by Terraform

To provide more context, the Lambda binary is zipped up through the archive_file resource. This resource looks for a file in the local directory that is named lambda_function.py , and in return creates a zip file named lambda.zip .

The aws_s3_bucket_object then uploads this zipped up file to an S3 bucket and drops off the unsigned artifact into a folder named unsigned/ .

Next, the resource aws_signer_signing_job is the resource that will take the file stored in the S3 bucket location unsigned/lambda.zip and generated a signed version for usage by the Lambda. If you look a the input parameters you can see that the signed file is being dropped off at the same bucket in the signed/ folder. You don’t have to keep the files in the same bucket, but for example purposes that is the current approach. The post signing job file will have a name along these lines :

signed/lambda-3ed11736-6655-4448-935d-659cd0428b90.zip

The signing job inserts a hash value into the file name along with a private key that be found inside the zip file in a folder named META_INF .

The last step is to reference the signed file in our Terraform template, more specifically, our Lambda resource. But how do we do that if the name is dynamic????

Easy, we can solve this challenge in the following manner.

A single input value for enabling code signing in a Lambda

We are simply using the output provided by the aws_signer_signing_job resource. That output looks like the following.

Outputs: signed-object = tolist([

{

"s3" = tolist([

{

"bucket" = "code-signing-demo"

"key" = "signed/9264bbee-9f5b-455d-9cfd-c3250e4679d1.zip"

},

])

},

])

So by using the following terraform syntax we can get the name of newly generated file. You can also do further name manipulation trough a locals variable if needed (hint).

aws_signer_signing_job.build_signing_job.signed_object[0]["s3"][0]["key"]

Note: The depends_on statements ensures the flow occurs in the right order.

Option 2- 100% Terraform Managed + Local-Exec

This option is an extension of Option 1, but in this scenario there are requirements that need to be met before the Lambda artifact is ready for upload to S3. This applicable if the Lambda is authored in a programming language that requires compilation and/or data_archive and aws_s3_bucket_object are not the preferred methods for deploying artifacts. Let’s a take a peek at the code below.

Local-Exec is doing build steps and uploading final artifact

Again, very similar to Option 1, except now we have a null_resource that is executing custom logic in a local-exec . In this example the Lambda is being zipped up using zip and the artifact is uploaded using the AWS CLI. The code inside the local-exec can be anything that makes sense for your use case. In this option, we are still leveraging the aws_signer_signing_job and referencing the signed file name using the same trick as earlier.

Option 3 — Non Terraform Managed

This is option is more applicable to larger projects/code bases where the Lambda artifact generation occurs outside the scope of Terraform, more than likely as a step/stage in a the pipeline.

Let’s assume in this option that before the Terraform code executes that in the pipeline the following step occurs through a bash script.

#!/bin/bash set -e zip lambda.zip ./lambda_function.py aws s3 cp --profile="demo" lambda.zip s3://code-signing-demo/unsigned/ aws signer start-signing-job --profile="demo" --profile-name="abc_20201129171057064200000001" --source "s3={bucketName='code-signing-demo',key='unsigned/lambda.zip',version="null"}" --destination "s3={bucketName='code-signing-demo',prefix=signed/lambda-}"

This bash script is zipping up the python file, uploading to S3, and then kicking off the AWS Code Signing job through the AWS CLI.

Note: The the code signing job is not managed by Terraform unlike the previous options.

The main challenge with this option is supplying the location/name of the signed zip file. Let’s take a peek at how we can solve this challenge.

Lambda binaries created and uploaded in a CI/CD pipeline

In this option, to get around the dynamic name challenge by leveraging two new resources, data aws_s3_bucket_object and two locals variables.

The data lookup for all S3 objects with a given prefix allows us to get all the signed zip files’s S3 path. We then store that output in a local variable named signedSourceList . We then defined another locals variable, lambdaSource that references the first index value of the signedSourceList variable. We can use index [0] because we know that we only have one singled signed file. Then on line 8 we pass in lambdaSource as the input value as it contains the S3 location of our signed file.

However, should you find yourself in a situation where you have more than one signed file and your Terraform template defines multiples Lambdas, you could solve it the following manner.

Fun with Terraform functions 😅