Skip to main content

Integration with AWS API Gateway

AWS API Gateway can be configured to trust X.509 certificates presented by SPIRL enabled workloads. SPIRL utilizes short-lived root certificates, and automates their creation and deprecation within the SPIRL components. Configuring an external service such as API Gateway requires the current valid set of valid trust anchors are synchronized into the API Gateway configuration, as new roots are introduces and old roots are deprecated.

SPIRL provides a tool that can be used to maintain the synchronization of the AWS API Gateway mutual TLS trust store with the SPIRL root certificates.

Running under AWS Lambda using Terraform

Requirements

  1. An internal Amazon Elastic Container Registry (ECR) repository located in the same region as the AWS Lambda function.
  2. An active SPIRL trust domain.

Copying the container image

Copy the container release from SPIRL to the internal ECR repository:

tip

Don't forget to replace the sample value of 11111111111.dkr.ecr.us-west-2 with your correct ECR repository hostname (your default private repository is AWS_ACCOUNT.dkr.ecr.REGION.amazonaws.com)

docker pull ghcr.io/spirl/spirl-sync:0.1.6 docker tag ghcr.io/spirl/spirl-sync:0.1.6 11111111111.dkr.ecr.us-west-2.amazonaws.com/spirl-sync:0.1.6 docker push 11111111111.dkr.ecr.us-west-2.amazonaws.com/spirl-sync:0.1.6

Gather information needed for later configuration

Gather the SPIFFE Bundle Endpoint that is created for the trust domain:

spirlctl trust-domain info example.com

Example

spirlctl trust-domain info example.com
Getting Trust Domain Info⠼
ID td-d3ornt0mnw
Name: example.com
Status: available
Self-Managed: false
SPIRL Agent Endpoint: td-d3ornt0mnw.agent.spirl.com:443
SPIFFE Bundle Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-d3ornt0mnw/bundle
JWT Issuer: https://fed.spirl.org/t-su8rvkjgix/td-d3ornt0mnw
JWKS Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-d3ornt0mnw/jwks
OIDC Discovery Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-d3ornt0mnw/.well-known/openid-configuration

Created At: 2025-01-08 14:50:10.306 +0000 UTC
Last Updated At: 2025-04-07 22:06:45.711 +0000 UTC

In the example we want to copy https://fed.spirl.org/t-su8rvkjgix/td-d3ornt0mnw/bundle for later use.

Example Lambda function creation with required permissions

resource "aws_lambda_function" "spirl_sync" {
function_name = "spirl-sync"
role = aws_iam_role.spirl_sync_lambda_exec.arn
package_type = "Image"
image_uri = "11111111111.dkr.ecr.us-west-2.amazonaws.com/spirl-sync:v0.0.0"

# Specify arm64 architecture, amd64 can also be used
architectures = ["arm64"]

# Lambda functions using container images don't use the handler and runtime parameters
# as they are defined in the container
timeout = 120
memory_size = 128

# Environment variables will be added here as needed
environment {
variables = {
# SYNC_TARGET tells spirl-sync that we are configuring an API gateway
SYNC_TARGET = "apigateway"

# BUNDLE_ENDPOINTS is a comma-separated list of federation endpoints from spirl.
# Set this to the SPIFFE bundle endpoint that was located above.
BUNDLE_ENDPOINTS = "https://fed.spirl.org/t-aaaaaaaaaa/td-bbbbbbbbbb/bundle"

# API_GATEWAY_ID is the API gateway this Lambda function should be configuring
API_GATEWAY_ID = aws_apigatewayv2_api.api_gateway.id

# S3_BUCKET_NAME is the S3 bucket that will be used to save the trust store
S3_BUCKET_NAME = aws_s3_bucket.api_gateway_trust_store.bucket

# S3_BUNDLE_KEY is the path within the bucket to save the trust store
S3_BUNDLE_KEY = "bundle.pem"

# DOMAIN_NAME is the domain name configuration that will be attached to the API gateway
DOMAIN_NAME = var.gateway_domain
}
}

depends_on = [aws_cloudwatch_log_group.spirl_sync]
}

# CloudWatch Log Group for Lambda function
resource "aws_cloudwatch_log_group" "spirl_sync" {
name = "/aws/lambda/spirl-sync"
retention_in_days = 30

# Optional: Add tags as needed
tags = {
Service = "spirl-sync"
}
}

# Execution Role for the spirl_sync Lambda function
resource "aws_iam_role" "spirl_sync_lambda_exec" {
name = "spirl_sync_lambda_exec_role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}

# Attach the AWSLambdaBasicExecutionRole to the Lambda function exec role
resource "aws_iam_role_policy_attachment" "spirl_sync_lambda_exec" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
role = aws_iam_role.spirl_sync_lambda_exec.name
}

# Set exec permissions on the Lambda function
# - Access to pull the spirl-sync container image
# - Access to the S3 bucket to store the trust-store needed by the API Gateway
# - Access to the API Gateway to patch / update the gateway with new versions
resource "aws_iam_role_policy" "spirl_sync_lambda_exec" {
name = "spirl_sync_lambda_exec_ecr_access"
role = aws_iam_role.spirl_sync_lambda_exec.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:GetAuthorizationToken"
]

# Replace with the ARN for the ECR repository holding the spirl-sync image
Resource = "arn:aws:ecr:us-west-2:11111111111:repository/spirl-sync"
},
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket",
"s3:GetObjectVersion",
]
Resource = [
# Replace with the S3 bucket arn / path that will hold the trust store
"${aws_s3_bucket.api_gateway_trust_store.arn}",
"${aws_s3_bucket.api_gateway_trust_store.arn}/*"
]
},
{
Effect = "Allow"
Action = [
"apigateway:GET",
"apigateway:PATCH",
"apigateway:PUT",
"apigateway:POST",
"apigateway:UPDATE",
"apigateway:AddCertificateToDomain",
"apigateway:RemoveCertificateFromDomain"
]

# Replace with the ARN of the domainname that will be doing the mTLS termination
Resource = "arn:aws:apigateway:us-west-2::/domainnames/sandbox.dev.spirl.net"
}
]
})
}

Test the Lambda function execution

Use the AWS console to test the Lamba function.

Running on a schedule

# EventBridge rule to trigger the Lambda function on a schedule
resource "aws_cloudwatch_event_rule" "spirl_sync_schedule" {
name = "spirl-sync-schedule"
description = "Schedule for running the spirl-sync function"
# Run every minute
schedule_expression = "rate(5 minutes)"
}

# EventBridge target that points to the Lambda function
resource "aws_cloudwatch_event_target" "spirl_sync_target" {
rule = aws_cloudwatch_event_rule.spirl_sync_schedule.name
target_id = "spirl_sync_lambda"
arn = aws_lambda_function.spirl_sync.arn
}


# Permission for EventBridge to invoke the Lambda function
resource "aws_lambda_permission" "allow_eventbridge" {
statement_id = "AllowExecutionFromEventBridge"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.spirl_sync.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.spirl_sync_schedule.arn
}