AWS Compute Blog

Bring your own client certificate for backend mTLS in Amazon API Gateway

Enterprises that use Amazon API Gateway in front of internal or partner backends often want to bring their own client certificate for backend mutual TLS (mTLS) authentication. During mTLS, the backend presents its own server certificate and also requests the caller to present a client certificate to validate it against a trusted certificate authority (CA). Until now, you could use only an API Gateway-generated, self-signed SSL certificate for the outbound connection, because there was no CA behind it for the backend to trust. Backends that enforce a specific corporate or partner CA reject that self-signed certificate, and the mutual TLS handshake fails. Bringing your own CA-signed certificate is necessary for scenarios such as migrating APIs off legacy gateways or meeting your internal PKI mandates that require certificates from an approved CA.

With API Gateway, you can now bring your own client certificate for backend mutual TLS (mTLS) authentication. You can either use a third-party certificate or a certificate issued by AWS Private Certificate Authority. If you’re using a third-party certificate, you must import the certificate in AWS Certificate Manager (ACM). Then you configure the ACM certificate ARN in your REST API stage. API Gateway presents that certificate during the backend mTLS handshake.

Solution overview

In this post, you build a REST API with an outbound mTLS connection using this newly launched API Gateway feature. This solution demonstrates an outbound mTLS connection between Amazon API Gateway and a backend application running on Amazon Elastic Container Service (Amazon ECS).

The following diagram shows the solution architecture.

Architecture diagram showing API Gateway presenting an ACM client certificate to a Network Load Balancer that forwards traffic to an NGINX sidecar and validator app on Amazon ECS Fargate, with certificates issued by AWS Private CA through ACM

The solution uses:

  1. AWS Private Certificate Authority with a root-subordinate CA hierarchy to issue both the client and server certificates through AWS Certificate Manager (ACM).

  2. Amazon API Gateway REST API stage configured with the ACM client certificate ARN (ClientCertificateId), so that the API Gateway presents the certificate during the outbound TLS handshake.

  3. Amazon ECS on AWS Fargate running an NGINX sidecar that holds the server certificate and validates the incoming client certificate against a CA bundle (root and subordinate chain).

A request goes through the following steps:

  1. Client application invokes the REST API exposed by API Gateway. The API Gateway stage is configured with an ACM client certificate ARN.

  2. API Gateway opens an outbound connection to the Network Load Balancer (NLB) to begin the TLS handshake. The API Gateway presents an ACM client certificate configured at the stage level when the backend requests one.

  3. The NLB listens for the incoming TCP request on port 443 and forwards the call to Amazon ECS Fargate. The NLB acts as a passthrough and does not terminate the TLS connection.

  4. The NGINX sidecar container running on Amazon ECS performs the inbound mTLS handshake:

  • NGINX presents the backend server certificate and verifies the client certificate against a mounted CA bundle (root and subordinate chain).

  • After verification, NGINX forwards the request and parsed certificate details to the validator app container over local HTTP.

  • The validator app re-checks the certificate validity window, matches the common name against an allowlist, and returns a structured JSON response.

Note: The NGINX sidecar is not mandatory for this flow. It demonstrates separation of concerns: NGINX handles the mTLS handshake, and the validator app contains the business logic.

Prerequisites for demo

To follow along, you need the following:

Environment setup

Run the following commands to set up the demo environment:

  1. Create a new folder and clone the GitHub repository:

    git clone https://github.com/aws-samples/sample-api-backend-mtls
    cd sample-api-backend-mtls
  2. Set the environment variables after replacing the placeholders:

    ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
    REGION=<Your AWS Region, for example, us-east-1>
    STACK_NAME=<Your stack name e.g. outbound-mtls-backend>

Build the container images

Run the following commands to create container images of NGINX sidecar container and the validator app containers:

  1. Create two Amazon Elastic Container Registry (Amazon ECR) repositories, one for NGINX and another for validator app containers respectively:

    NGINX_REPO_URI=$(aws ecr create-repository \
      --repository-name $STACK_NAME-nginx-sidecar \
      --image-tag-mutability IMMUTABLE \
      --image-scanning-configuration scanOnPush=true \
      --region "$REGION" \
      --query "repository.repositoryUri" --output text)
    
    VALIDATOR_REPO_URI=$(aws ecr create-repository \
      --repository-name $STACK_NAME-validator-app \
      --image-tag-mutability IMMUTABLE \
      --image-scanning-configuration scanOnPush=true \
      --region "$REGION" \
      --query "repository.repositoryUri" --output text)
    
    aws ecr get-login-password --region "$REGION" \
      | docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"
  2. Build and push the NGINX and validator app containers:

    docker build --platform linux/amd64 -t $STACK_NAME-nginx-sidecar nginx/
    docker tag $STACK_NAME-nginx-sidecar:latest "${NGINX_REPO_URI}:latest"
    docker push "${NGINX_REPO_URI}:latest"
    docker build --platform linux/amd64 -t $STACK_NAME-validator-app validator_app/
    docker tag $STACK_NAME-validator-app:latest "${VALIDATOR_REPO_URI}:latest"
    docker push "${VALIDATOR_REPO_URI}:latest"

Deploy and test the solution

You first deploy the stack without the client certificate configured in the API Gateway and perform negative testing. The mTLS handshake will fail because of a missing client certificate in the request. Then you update the stack to configure client certificate in API Gateway stage and retest mTLS.

  1. Run the following command to build and deploy the overall stack without client certificate configured at API Gateway stage:

    sam build
    sam deploy \
      --stack-name $STACK_NAME \
      --resolve-s3 \
      --capabilities CAPABILITY_IAM \
      --region "$REGION" \
      --parameter-overrides \
      NginxRepositoryUri="$NGINX_REPO_URI" \
      ValidatorRepositoryUri="$VALIDATOR_REPO_URI" \
      EnableOutboundMtls=false
  2. Wait for the task to reach RUNNING and pass its target group health check:

    EcsClusterName=$(aws cloudformation describe-stacks \
      --stack-name $STACK_NAME --region "$REGION" \
      --query "Stacks[0].Outputs[?OutputKey=='EcsClusterName'].OutputValue" \
      --output text)
    
    TargetGroupArn=$(aws cloudformation describe-stacks \
      --stack-name $STACK_NAME --region "$REGION" \
      --query "Stacks[0].Outputs[?OutputKey=='TargetGroupArn'].OutputValue" \
      --output text)
    
    aws ecs list-tasks --cluster "$EcsClusterName" --region "$REGION"
    
    aws elbv2 describe-target-health \
      --target-group-arn "$TargetGroupArn" --region "$REGION"
  3. Capture the front API invoke URL from the stack outputs:

    FRONT_API_URL=$(aws cloudformation describe-stacks \
      --stack-name $STACK_NAME --region "$REGION" \
      --query "Stacks[0].Outputs[?OutputKey=='FrontApiUrl'].OutputValue" \
      --output text)
    
    NLB_DNS_NAME=$(aws cloudformation describe-stacks \
      --stack-name $STACK_NAME --region "$REGION" \
      --query "Stacks[0].Outputs[?OutputKey=='NlbDnsName'].OutputValue" \
      --output text)
    
    FRONT_CLIENT_CERT_ARN=$(aws cloudformation describe-stacks \
      --stack-name $STACK_NAME --region "$REGION" \
      --query "Stacks[0].Outputs[?OutputKey=='FrontClientCertArn'].OutputValue" \
      --output text)
    
    FRONT_API_ID=$(aws cloudformation describe-stacks \
      --stack-name $STACK_NAME --region "$REGION" \
      --query "Stacks[0].Outputs[?OutputKey=='FrontApiId'].OutputValue" \
      --output text)
  4. Wait a minute or two after the stack finishes, then invoke the front API:

    curl -v "$FRONT_API_URL"

    The following is the NGINX configuration for mTLS:

    ...
    server {
        listen 443 ssl;
        # Server identity (issued by the private CA).
        ssl_certificate /etc/nginx/certs/server.crt;
        ssl_certificate_key /etc/nginx/certs/server.key;
        # Inbound mutual TLS: require and validate the client certificate
        # against the CA bundle (root + subordinate CA chain).
        ssl_client_certificate /etc/nginx/certs/ca_bundle.pem;
        ssl_verify_client on;
        ssl_verify_depth 2;
        ssl_protocols TLSv1.2;
    ...}

    The curl command returns HTTP/2 400, with a response body containing 400 No required SSL certificate was sent. Because the API Gateway is not presenting a client certificate on the outbound handshake, the NGINX sidecar container in Amazon ECS rejects the mTLS connection. The following screenshot shows the response:

    Terminal response showing an HTTP/2 400 error with the message No required SSL certificate was sent
  5. Now redeploy the solution with outbound mTLS enabled:

    sam deploy \
      --stack-name $STACK_NAME \
      --resolve-s3 \
      --capabilities CAPABILITY_IAM \
      --region "$REGION" \
      --parameter-overrides \
      NginxRepositoryUri="$NGINX_REPO_URI" \
      ValidatorRepositoryUri="$VALIDATOR_REPO_URI" \
      EnableOutboundMtls=true
  6. Wait a minute or two after the stack finishes, then invoke the front API again:

    curl -v "$FRONT_API_URL"

    Because the client certificate is now presented during the mTLS handshake, the handshake completes successfully, as shown in the following response snippet:

    Terminal response showing a successful mTLS handshake and an HTTP 200 response from the backend

Automatic certificate renewal

When a certificate changes in ACM, API Gateway detects the update and propagates the new certificate automatically. You do not redeploy the stage, and the API experiences no downtime during rotation. Certificate propagation is eventually consistent. During an update, the backend might briefly receive either the old or the new certificate. ACM also emits certificate expiration notifications through Amazon EventBridge, which you can use to set alarms before a certificate expires.

Clean up

If you followed along only for demonstration purposes, to avoid incurring future charges, run the following commands to delete the resources created in this demo:

  1. Clean up the S3 buckets:

    NLB_LOGS_BUCKET=$(aws cloudformation describe-stacks \
      --stack-name $STACK_NAME --region $REGION \
      --query "Stacks[0].Outputs[?OutputKey=='NlbAccessLogsBucketName'].OutputValue" \
      --output text)
    
    aws s3api list-object-versions --bucket "$NLB_LOGS_BUCKET" \
      --query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}' \
      --output json | \
      jq -c '.Objects[]? // empty' | \
      while read -r obj; do
        aws s3api delete-object --bucket "$NLB_LOGS_BUCKET" \
          --key "$(echo "$obj" | jq -r .Key)" \
          --version-id "$(echo "$obj" | jq -r .VersionId)" \
          --region $REGION
      done
    
    aws s3api list-object-versions --bucket "$NLB_LOGS_BUCKET" \
      --query '{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}' \
      --output json | \
      jq -c '.Objects[]? // empty' | \
      while read -r obj; do
        aws s3api delete-object --bucket "$NLB_LOGS_BUCKET" \
          --key "$(echo "$obj" | jq -r .Key)" \
          --version-id "$(echo "$obj" | jq -r .VersionId)" \
          --region $REGION
      done
  2. Delete the stack:

    sam delete --stack-name $STACK_NAME --region $REGION --no-prompts
  3. Delete the ECR repository:

    aws ecr delete-repository --repository-name $STACK_NAME-nginx-sidecar --force --region $REGION
    aws ecr delete-repository --repository-name $STACK_NAME-validator-app --force --region $REGION

Conclusion

In this post, you configured a REST API with an outbound mTLS connection using Amazon API Gateway and an ECS Fargate backend. With this new feature launch in API Gateway, you can now bring your own client certificate for outbound mTLS handshake for your REST APIs. You can now meet your internal PKI mandates to authenticate backends that pin a specific certificate issuer.

To get started, import a certificate from your own PKI into ACM and configure your API Gateway REST API stage for outbound mTLS authentication. For more information, see Present client certificates to backend services with mutual TLS in API Gateway. If you have feedback about this post, leave it in the comments section. For technical questions, you can start a thread on AWS re:Post.

Further reading