Code Signing for Lambda ๐Ÿ”

meme

A couple of months ago, I was working on increasing the security posture of our AWS Lambda functions. Code Signing was one of the AWS features that I tested out. In this post I will talk a bit about my experience with Code Signing for Lambda.

Introduction Link to heading

AWS Code Signing for Lambda was released 4 years ago in 2020. This ensures that only trusted code can be deployed to the Lambda functions by validating the code signature with the signing profile. AWS Signer is one of the requirement in implementing code signing. The good news is that there is no additional charge to use AWS Signer with AWS Lambda. ๐ŸŽ‰

Setup Link to heading

This terraform code snippet below will get you up and running with Code Signing for Lambda.

# main.tf

provider "aws" {
  region = "us-east-1"
}

data "archive_file" "dummy" {
  output_path = "${path.module}/dummy.zip"
  type        = "zip"

  source {
    content  = "dummy"
    filename = "dummy.py"
  }
}

resource "random_id" "this" {
  byte_length = 2
}

module "signing_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "~> 4.1.0"

  bucket        = "signing-${random_id.this.dec}"
  force_destroy = true

  block_public_acls   = true
  block_public_policy = true
  ignore_public_acls  = true

  versioning = {
    status = true
  }

  lifecycle_rule = [
    {
      id      = "cleanOldObjects"
      enabled = true

      expiration = {
        days = 1
      }
      noncurrent_version_expiration = {
        days = 1
      }
      abort_incomplete_multipart_upload_days = 1
    }
  ]
}

resource "aws_signer_signing_profile" "this" {
  platform_id = "AWSLambda-SHA384-ECDSA"

  signature_validity_period {
    value = 1
    type  = "MONTHS"
  }
}

resource "aws_s3_object" "this" {
  bucket = module.signing_bucket.s3_bucket_id
  key    = "dummy.zip"
  source = data.archive_file.dummy.output_path
}

resource "aws_signer_signing_job" "this" {
  profile_name = aws_signer_signing_profile.this.name

  source {
    s3 {
      bucket  = module.signing_bucket.s3_bucket_id
      key     = aws_s3_object.this.id
      version = aws_s3_object.this.version_id
    }
  }

  destination {
    s3 {
      bucket = module.signing_bucket.s3_bucket_id
      prefix = "signed/"
    }
  }

  ignore_signing_job_failure = false
}

resource "aws_lambda_code_signing_config" "this" {
  description = "Test Code Signing Config for Lambda"

  allowed_publishers {
    signing_profile_version_arns = [
      aws_signer_signing_profile.this.version_arn,
    ]
  }

  policies {
    untrusted_artifact_on_deployment = "Enforce"
  }
}

module "lambda" {
  source  = "terraform-aws-modules/lambda/aws"
  version = "~> 7.2.0"

  function_name           = "lambda-code-signing-test"
  description             = "Test Lambda Function for Code Signing"
  handler                 = "index.handler"
  runtime                 = "python3.8"
  create_package          = false
  code_signing_config_arn = aws_lambda_code_signing_config.this.arn

  s3_existing_package = {
    bucket = aws_signer_signing_job.this.signed_object[0].s3[0].bucket
    key    = aws_signer_signing_job.this.signed_object[0].s3[0].key
  }
}

Figure 1. Sample terraform code to provision the resources

After applying the terraform code above, you will see the following resources provisioned in the us-east-1 region:

lambda function Figure 2. Lambda function with code signing configured

lambda code signing config Figure 3. Lambda code signing config with Enforce policy

s3 signing bucket Figure 4. S3 signing bucket

signer signing profile Figure 5. Signer signing profile

Here’s an example when a rogue actor (has access to the lambda function without permission to the S3 bucket or AWS Signer) tries to update the lambda function code with an unsigned code:

lambda update unsigned code Figure 6. Lambda function failed code update

The code update fails as the Lambda function do not accept code that are unsigned or signed by Signing profile that are not defined in the Lambda code signing configuration.

We can see how it’s easy to increase the security of the Lambda function by enabling signature validation.

Cloudtrail also records information related to the function code update in the UpdateFunctionCode20150331v2 event. Under the event record, the signatureStatus will either show VALID or MISMATCH (if signature validation set to Warn and unsigned code is deployed).

lambda update unsigned code Figure 7. Cloudtrail UpdateFunctionCode20150331v2 event

lambda update unsigned code Figure 8. Cloudtrail UpdateFunctionCode20150331v2 event record

Note Link to heading

  1. Lambda Code Signing Config can be set to either Warn or Enforce.
    Warn will not prevent deployment from failing if signature validation fails.
  2. Any Lambda layer or extensions enabled on the Lambda function should be signed as well.
  3. The terraform code in Figure 1 doesn’t create the IAM role for GitHub Action to trigger signing job and update lambda function code.

Deployment Flow Link to heading

code signing deployment flow Figure 9. Deployment flow from repository to Lambda function

Figure 9 shows the deployment flow for deploying signed code to AWS Lambda. When a deployment is triggered from the CI/CD pipeline (ie. GitHub Action) to deploy signed code to Lambda, the following flows will happen:

  1. Upload zipped code to S3 bucket for signing.
  2. Trigger AWS Signer signing job to sign the code from S3 bucket with signing profile.
  3. AWS Signer signs the code and uploads it to S3 bucket.
  4. Update Lambda function code with the signed code from S3 bucket.
  5. Lambda function retrieve signed code from S3 bucket, validates the signature, accept the update only if the signature is valid.

Limitation Link to heading

There are some limitation when having code signing enabled for Lambda.

  1. More steps in the deployment process
    Usual lambda deployment only involves updating the lambda function code with the zipped code. The image in figure 6 shows that there are additional steps from uploading to s3 to triggering the signing job.

  2. Lambda extension needs to be signed with the signing profile defined in the code signing config.
    This includes AWS managed Lambda extensions such as AWS Parameter and Secrets Lambda extension as they are deployed as Lambda Layer. This can be done by downloading the layer using the AWS CLI and curl, and then signing the layer (this is a pain to deal with).
    All the Lambda Layers attached to the Lambda’s are subjected to signature validation when the code signing config is set.

Conclusion Link to heading

AWS Code Signing for Lambda is a great feature to add to your Lambda functions to increase your organisation security posture. You should however be aware of the limitation that comes especially if you have some extensions enabled for the Lambda function. The lack of native support for signing Lambda extensions will cause an additional overhead in managing the signing process.

Nonetheless, give it a try and let me know what you think.