Integration with AWS API Gateway
AWS API Gateway can be configured to trust X.509 certificates presented by Defakto-enabled workloads. Defakto utilizes short-lived root certificates and automates their creation and deprecation within the Defakto components.
When configuring an external service such as API Gateway, the current valid set of trust anchors must be synchronized into the API Gateway configuration as new root certificates are introduced and old ones expire.
Defakto provides a tool called spirl-sync that automatically maintains synchronization between the AWS API Gateway mutual TLS trust store and the Defakto root certificates.
What is spirl-sync?​
spirl-sync is a lightweight tool that:
- Monitors your Defakto trust domain federation endpoint for certificate changes
- Automatically downloads new root certificates
- Updates your AWS API Gateway's trust store with the latest certificates
- Removes expired certificates from the AWS API Gateway's trust store
This ensures your API Gateway always trusts the current Defakto certificates without manual intervention.
Running under AWS Lambda using Terraform​
This guide shows how to deploy spirl-sync as an AWS Lambda function using Terraform. The Lambda function will run periodically to keep your API Gateway's trust store synchronized with SPIRL's root certificates.
What you'll need​
Before starting, ensure you have:
- An internal Amazon Elastic Container Registry (ECR) repository located in the same region as your planned AWS Lambda function
- An active Defakto trust domain
- Basic familiarity with Terraform and AWS Lambda
Step 1: Copy the container image to your ECR repository​
First, copy the spirl-sync container image from Defakto's registry to your internal ECR repository:
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.7 docker tag ghcr.io/spirl/spirl-sync:0.1.7 11111111111.dkr.ecr.us-west-2.amazonaws.com/spirl-sync:0.1.7 docker push 11111111111.dkr.ecr.us-west-2.amazonaws.com/spirl-sync:0.1.7
Step 2: Gather your Defakto configuration information​
You'll need to find the SPIFFE Bundle Endpoint for your trust domain. This endpoint provides the certificates that spirl-sync will synchronize to API Gateway.
Run this command to get your trust domain information:
spirlctl trust-domain info example.com
Example output:
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
From this output, copy the SPIFFE Bundle Endpoint URL (in this example: https://fed.spirl.org/t-su8rvkjgix/td-d3ornt0mnw/bundle) - you'll need this for the Lambda configuration.
Step 3: Create the Lambda function with Terraform​
This Terraform configuration creates a Lambda function that runs spirl-sync to keep your API Gateway synchronized with SPIRL certificates.
Important values to customize:
- Replace
11111111111.dkr.ecr.us-west-2.amazonaws.com/spirl-sync:v0.0.0with your actual ECR repository details - Update the
BUNDLE_ENDPOINTSvalue with your SPIFFE Bundle Endpoint from Step 2 - Set
API_GATEWAY_IDto reference your API Gateway - Configure
S3_BUCKET_NAMEandDOMAIN_NAMEfor your environment
resource "aws_lambda_function" "spirl_sync" {
function_name = "spirl-sync"
role = aws_iam_role.spirl_sync_lambda_exec.arn
package_type = "Image"
# Replace this with the full path you uploaded in Step 1
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
# Note: Initial configuration of an API Gateway Domain Name for mTLS can take more than 5 minutes
timeout = 600
memory_size = 128
# Environment variables configure how spirl-sync operates
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 Defakto.
# This is the list of source of the certificates and all certificates in the federation endpoint will be included.
# Replace this with your SPIFFE bundle endpoint from Step 2
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
# The Lambda needs permissions to:
# - Pull the spirl-sync container image from ECR
# - Access the S3 bucket to store the trust store needed by API Gateway
# - Update the API Gateway configuration with new certificate 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 domain name that will be doing the mTLS termination
Resource = "arn:aws:apigateway:us-west-2::/domainnames/example.dev.spirl.net"
}
]
})
}
Step 4: Test the Lambda function​
Once deployed, test your Lambda function to ensure it's working correctly:
- Open the AWS Lambda console
- Find your
spirl-syncfunction - Click the Test button
- No test parameters are needed - just click Test and monitor the execution logs
The function should complete successfully and you should see logs indicating that it synchronized certificates with your API Gateway.
Step 5: Set up automatic synchronization​
To keep your certificates current, configure the Lambda function to run automatically on a schedule. This example runs every 5 minutes, but you can adjust the frequency based on your security requirements.
# 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"
# Runs every 5 minutes - adjust frequency as needed
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
}
Step 6: Handle Terraform conflicts​
When using Terraform to manage your API Gateway domain name, you need to prevent conflicts between Terraform and spirl-sync. Both tools will try to manage the same configuration, which will cause an outage when terraform removes the mTLS configuration.
The solution is to tell Terraform to ignore changes that spirl-sync makes to the mutual TLS configuration. spirl-sync only manages the mutual_tls_authentication portion of your domain configuration, so it's safe to ignore these changes in Terraform.
resource "aws_apigatewayv2_domain_name" "api_gateway" {
domain_name = "example.com"
domain_name_configuration {
certificate_arn = "arn://example/..."
endpoint_type = "REGIONAL"
security_policy = "TLS_1_2"
}
# This lifecycle rule prevents Terraform from trying to remove the mutual_tls_authentication
# configuration that spirl-sync adds. Without this, Terraform would remove the mTLS
# configuration on each run, causing service outages.
lifecycle {
ignore_changes = [ mutual_tls_authentication ]
}
}