AWS ACM · 2026-10-09 · ACM arc

TLS for MCP Servers on AWS: ACM Certificate Provisioning, ALB and CloudFront HTTPS Termination, and Private CA mTLS

The single most common TLS misconfiguration for MCP servers on AWS is requesting an ACM certificate in the wrong region for CloudFront: if the certificate is in us-west-2 and the CloudFront distribution is global, the certificate will not appear in the distribution's certificate selector — there is no error, just a silent absence. CloudFront's control plane only reads certificates from us-east-1, so every certificate destined for a CloudFront distribution must be requested with --region us-east-1 regardless of where the MCP server's origin lives. AWS Certificate Manager (ACM) covers the full TLS lifecycle for MCP server deployments across four distinct topics: (1) certificate provisioning and auto-renewal — free public certificates issued by Amazon Trust Services; DNS validation via a permanent CNAME record (DNS validation fully automates renewal; email validation requires a manual click every 13 months); EC_prime256v1 ECDSA P-256 as the recommended key algorithm (10–20× faster TLS handshakes than RSA 2048; same security margin; all modern AI-agent and developer-SDK clients support it; request a new cert to migrate — key algorithm cannot be changed in-place); certificate validity fixed at 13 months (395 days per CA/Browser Forum baseline); auto-renewal starts 60 days before expiry using the validation CNAME that's already in DNS; renewal fails silently if the CNAME is deleted during a DNS migration or zone cleanup; monitor with the AWS/CertificateManager DaysToExpiry metric (automatic, no setup) plus an EventBridge rule on ACM Certificate Approaching Expiration events at 45/30/15/7/3/1 days before expiry; ACM certificates are not exportable — they bind only to ALB, NLB, API Gateway, CloudFront, and AppSync; (2) ALB HTTPS termination — the standard pattern for ECS/EC2/Lambda-backed MCP servers: ALB handles TLS, backend containers receive plain HTTP inside the VPC; attach ACM certificate to HTTPS listener with ELBSecurityPolicy-TLS13-1-2-2021-06 (TLS 1.2 minimum, TLS 1.3 supported — excludes all deprecated cipher suites; do not use ELBSecurityPolicy-2016-08 which still supports TLS 1.0); add port 80 listener with Type=redirect HTTP_301 to HTTPS; SNI lets one HTTPS listener serve up to 25 domains with separate certificates; certificate rotation with zero downtime: add new cert as additional SNI certificate → set as default → remove old cert; ALB critical gotcha for SSE-based MCP servers: default idle timeout is 60 seconds — SSE connections with no data activity will be terminated mid-stream; set idle_timeout.timeout_seconds to 300+ for SSE; WebSocket upgrades are automatic when target protocol is HTTP; (3) CloudFront edge TLS — termination at global edge nodes for low-latency worldwide access, DDoS protection via AWS Shield Standard (included free), and optional caching for static MCP assets; us-east-1 certificate hard requirement for all CloudFront distributions regardless of origin region; SSLSupportMethod: sni-only is mandatory for custom domains (the alternative vip costs $600/month); MinimumProtocolVersion: TLSv1.2_2021 for modern cipher suites; ViewerProtocolPolicy: redirect-to-https for HTTP-to-HTTPS redirect at the edge; CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad (CachingDisabled managed policy) for all MCP API endpoints — caching a tool response serves stale data to subsequent clients; OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac (AllViewer) to forward all headers including Authorization and MCP session headers to origin; HttpVersion: http2and3 enables HTTP/3 QUIC for improved performance with concurrent MCP tool calls; CloudFront origin read timeout maximum 60 seconds — MCP tools running longer than 60 seconds must use async patterns (return task ID, poll for result); SSE streams require CachingDisabled and keepalive comments every 30 seconds; CloudFront certificate propagation takes up to 15 minutes globally after ACM issues a renewed certificate; and (4) ACM Private CA for mTLS — when service-to-service authentication via certificate is required (not API key), when direct EC2/container TLS termination is needed (ACM public certs cannot be exported), or when custom certificate validity periods are required; Private CA costs $400/month per active CA plus $0.75/1,000 certificates; two-tier hierarchy is mandatory (ROOT CA → SUBORDINATE CA — never issue end-entity certificates from root; keep root disabled between uses); ECS sidecar pattern for ephemeral certificate provisioning (cert-provisioner container generates key + CSR, issues 4-hour certificate from Private CA, writes to shared volume, mcp-server container starts after SUCCESS); mTLS on ALB: create trust store from CA bundle in S3 → modify-listener Mode=verify; ALB forwards client certificate details in X-Amzn-Mtls-Clientcert-* headers so the backend can implement fine-grained authorization by CN; short-lived certificates (hours) are preferred over CRL revocation for ephemeral workloads. This guide synthesizes all four topics into three structural patterns: certificate provisioning and lifecycle management, choosing the right TLS termination layer (ALB vs CloudFront), and internal mTLS with Private CA.

TL;DR

Pattern 1 — Certificate Provisioning and Auto-Renewal

DNS validation: the one decision that makes everything else automatic

ACM certificates require domain ownership validation before issuance. The choice between DNS and email validation is irreversible per-certificate and determines whether you ever need to manually intervene in the certificate lifecycle again. DNS validation adds a CNAME record to your DNS zone once, and ACM queries that record for every subsequent renewal — you never touch it again. Email validation sends a confirmation email for every renewal cycle (every 13 months), requiring a human to click a link within 72 hours. For any MCP server with more than one certificate or more than one operator, email validation will eventually cause an outage when a renewal email is missed.

# Request a certificate with DNS validation and ECDSA P-256 key
aws acm request-certificate \
  --domain-name api.mcp.example.com \
  --subject-alternative-names "*.mcp.example.com" "mcp.example.com" \
  --validation-method DNS \
  --key-algorithm EC_prime256v1 \
  --tags Key=Service,Value=mcp-server Key=Environment,Value=production \
  --region us-east-1

# Retrieve the DNS validation CNAME to add to your DNS zone
aws acm describe-certificate \
  --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/abc-123 \
  --query 'Certificate.DomainValidationOptions[].ResourceRecord'
# Add this CNAME to your DNS zone permanently — never delete it

# Automate for Route 53 hosted zones
HOSTED_ZONE_ID=$(aws route53 list-hosted-zones-by-name \
  --dns-name mcp.example.com \
  --query 'HostedZones[0].Id' --output text | sed 's|/hostedzone/||')

RECORD=$(aws acm describe-certificate \
  --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/abc-123 \
  --query 'Certificate.DomainValidationOptions[0].ResourceRecord')

aws route53 change-resource-record-sets \
  --hosted-zone-id $HOSTED_ZONE_ID \
  --change-batch "{\"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{
    \"Name\":$(echo $RECORD | jq '.Name'),
    \"Type\":\"CNAME\",
    \"TTL\":300,
    \"ResourceRecords\":[{\"Value\":$(echo $RECORD | jq '.Value')}]
  }}]}"

aws acm wait certificate-validated \
  --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/abc-123

One CNAME record validates both *.mcp.example.com and mcp.example.com — wildcard SANs share a validation record with their base domain. For multi-region deployments, request the same domain certificate in each region; the CNAME value is deterministic per domain (not per certificate ARN), so the validation record added for us-east-1 automatically validates certificates requested in us-west-2 and eu-west-1 for the same domain.

Key algorithm selection: EC_prime256v1 for all new deployments

The key algorithm affects every TLS handshake your MCP server initiates. For MCP servers under concurrent load — multiple AI agents calling tools simultaneously — the handshake cost compounds. ECDSA P-256 certificates are 10–20× faster to sign than RSA 2048 due to the underlying cryptographic operations, produce smaller certificate sizes (fewer bytes in the TLS ClientHello), and provide equivalent security for all current threat models. The only reason to choose RSA 2048 is compatibility with legacy TLS clients that predate ECDSA support — modern MCP clients (AI agent SDKs, developer HTTP libraries) all support ECDSA.

AlgorithmCLI flagHandshake speedCertificate sizeRecommendation
RSA 2048RSA_2048Baseline~2 KBDefault; only if legacy client compatibility required
RSA 4096RSA_40964× slower than RSA 2048~4 KBNot recommended — marginal security gain, significant handshake cost
EC P-256 (ECDSA)EC_prime256v110–20× faster than RSA 2048~0.5 KBBest choice for MCP server deployments
EC P-384 (ECDSA)EC_secp384r1Slightly slower than P-256~0.7 KBOnly if P-384 is a specific compliance requirement

Key algorithm cannot be changed after issuance. To migrate from RSA to ECDSA on a running ALB: request a new ECDSA certificate, add it as an additional SNI certificate on the listener, set it as the new default, then remove the old certificate — no downtime, ALB negotiates the new certificate immediately for new connections.

Certificate expiry monitoring: don't rely on ACM auto-renewal alone

ACM auto-renewal is not a guarantee — it is a best-effort process that fails when the DNS validation CNAME has been deleted. The most common cause is a DNS provider migration where the operator migrates all "real" records and forgets about ACM validation CNAMEs because they look like noise. The certificate then enters PENDING_VALIDATION status 60 days before expiry, stays there, and expires without any console notification unless you have an alarm configured.

# CloudWatch alarm on DaysToExpiry metric — fires at 60 days to give time to fix
# The DaysToExpiry metric is published automatically by ACM with no setup required
aws cloudwatch put-metric-alarm \
  --alarm-name ACMCertExpiry60Days \
  --alarm-description "ACM certificate expiring within 60 days — verify renewal status" \
  --metric-name DaysToExpiry \
  --namespace AWS/CertificateManager \
  --dimensions Name=CertificateArn,Value=arn:aws:acm:us-east-1:123456789012:certificate/abc-123 \
  --statistic Minimum \
  --period 86400 \
  --evaluation-periods 1 \
  --threshold 60 \
  --comparison-operator LessThanOrEqualToThreshold \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:cert-expiry-alerts \
  --treat-missing-data breaching  # alarm if metric stops publishing

# EventBridge rule for ACM-native expiry events (45/30/15/7/3/1 days before expiry)
aws events put-rule \
  --name ACMCertExpiryNotification \
  --event-pattern '{
    "source": ["aws.acm"],
    "detail-type": ["ACM Certificate Approaching Expiration"]
  }' \
  --state ENABLED

aws events put-targets \
  --rule ACMCertExpiryNotification \
  --targets '[{
    "Id": "SendToSNS",
    "Arn": "arn:aws:sns:us-east-1:123456789012:cert-expiry-alerts"
  }]'

# Check renewal status for a specific certificate
aws acm describe-certificate \
  --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/abc-123 \
  --query 'Certificate.RenewalSummary'
# RenewalStatus: PENDING_AUTO_RENEWAL / PENDING_VALIDATION / SUCCESS / FAILED

The CloudWatch DaysToExpiry alarm with treat-missing-data breaching is the most robust approach: if ACM stops publishing the metric (certificate deleted, region issue), the alarm fires rather than remaining OK. Pair it with the EventBridge rule to catch cases where ACM detects a renewal problem internally before the 60-day window. AliveMCP also monitors TLS certificate validity as part of MCP server health checks — if a certificate within 14 days of expiry is detected on any monitored endpoint, it fires an alert regardless of how the certificate was provisioned (ACM, Let's Encrypt, or custom CA).

Pattern 2 — Choosing Between ALB and CloudFront for TLS Termination

ALB: the standard choice for MCP server HTTPS

An Application Load Balancer with an ACM certificate is the right TLS termination layer for most MCP server deployments. The ALB terminates TLS, and backend containers receive plain HTTP inside the VPC — no TLS handling required in the MCP server code. ACM auto-renewal is transparent to the ALB: when ACM issues a renewed certificate, the ALB picks it up automatically without any listener modification or deployment.

# Create ALB with HTTPS listener and HTTP redirect
ALB_ARN=$(aws elbv2 create-load-balancer \
  --name mcp-server-alb \
  --subnets subnet-public-1a subnet-public-1b \
  --security-groups sg-alb-id \
  --scheme internet-facing \
  --type application \
  --query 'LoadBalancers[0].LoadBalancerArn' --output text)

# Enable deletion protection immediately
aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn $ALB_ARN \
  --attributes Key=deletion_protection.enabled,Value=true

# Create HTTPS listener with the recommended TLS policy
aws elbv2 create-listener \
  --load-balancer-arn $ALB_ARN \
  --protocol HTTPS \
  --port 443 \
  --certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/abc-123 \
  --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN

# Create HTTP redirect listener
aws elbv2 create-listener \
  --load-balancer-arn $ALB_ARN \
  --protocol HTTP \
  --port 80 \
  --default-actions '[{
    "Type": "redirect",
    "RedirectConfig": {"Protocol":"HTTPS","Port":"443","StatusCode":"HTTP_301"}
  }]'

# CRITICAL for SSE-based MCP servers: increase idle timeout beyond default 60s
# Default 60s terminates SSE connections with no data activity mid-session
aws elbv2 modify-load-balancer-attributes \
  --load-balancer-arn $ALB_ARN \
  --attributes Key=idle_timeout.timeout_seconds,Value=300

The SSE idle timeout is the most commonly missed ALB configuration for MCP servers. An SSE connection that receives no data for 60 seconds is terminated by the ALB with no error visible to the client — the connection silently closes. For MCP tools that poll or wait between events (e.g., a tool waiting for an async operation to complete), 60 seconds is easy to exceed. Increase the ALB idle timeout to at least 300 seconds, or add periodic keepalive SSE comments (: keepalive\n\n) from the server side to reset the idle timer without requiring an actual data event.

CloudFront: when global edge and DDoS protection are worth the constraints

CloudFront adds a global edge network in front of your MCP server origin, reducing latency for clients connecting from regions far from the origin. It also includes AWS Shield Standard (basic DDoS protection) at no additional charge. The tradeoffs are the 60-second origin read timeout ceiling and the us-east-1 certificate requirement — both of which affect MCP server architecture in non-obvious ways.

# Request CloudFront certificate — MUST be in us-east-1
aws acm request-certificate \
  --domain-name mcp.example.com \
  --subject-alternative-names "*.mcp.example.com" \
  --validation-method DNS \
  --key-algorithm EC_prime256v1 \
  --region us-east-1    # Required — CloudFront ignores certs from all other regions

# Create CloudFront distribution with correct settings for MCP API
aws cloudfront create-distribution \
  --distribution-config '{
    "CallerReference": "mcp-server-cf",
    "Comment": "MCP server edge TLS",
    "DefaultCacheBehavior": {
      "TargetOriginId": "mcp-server-alb",
      "ViewerProtocolPolicy": "redirect-to-https",
      "AllowedMethods": {
        "Quantity": 7,
        "Items": ["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"],
        "CachedMethods": {"Quantity": 2, "Items": ["GET","HEAD"]}
      },
      "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
      "OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac",
      "Compress": true
    },
    "Origins": {
      "Quantity": 1,
      "Items": [{
        "Id": "mcp-server-alb",
        "DomainName": "mcp-alb-123456.us-east-1.elb.amazonaws.com",
        "CustomOriginConfig": {
          "HTTPPort": 80,
          "HTTPSPort": 443,
          "OriginProtocolPolicy": "https-only",
          "OriginSSLProtocols": {"Quantity": 1, "Items": ["TLSv1.2"]},
          "OriginReadTimeout": 60
        }
      }]
    },
    "Aliases": {"Quantity": 1, "Items": ["mcp.example.com"]},
    "ViewerCertificate": {
      "ACMCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/abc-123",
      "SSLSupportMethod": "sni-only",
      "MinimumProtocolVersion": "TLSv1.2_2021",
      "CertificateSource": "acm"
    },
    "Enabled": true,
    "HttpVersion": "http2and3",
    "IsIPV6Enabled": true
  }'

The managed CachingDisabled policy (4135ea2d-6df8-44a3-9df3-4b5a84be39ad) sets TTL to zero for all cached responses — every request passes through to the MCP server origin. This is non-negotiable for MCP API endpoints: tool responses are per-session and per-request; caching a tool response and serving it to a different client is a data correctness bug, not just a performance issue. Use X-Cache: Miss from cloudfront in response headers to verify that no API responses are being cached.

Comparison: ALB vs CloudFront for MCP server TLS

FactorALBCloudFront
Certificate region requirementSame region as ALBMust be us-east-1
SSE connection supportYes — increase idle_timeout to 300+Limited — 60s origin read timeout; use keepalive comments
WebSocket supportAutomatic — ALB detects Upgrade headerAutomatic — requires CachingDisabled + AllViewer policy
DDoS protectionNone (add AWS WAF separately)AWS Shield Standard included
Global edge latencyNo — single regionYes — 400+ edge locations
Maximum request duration4,000 seconds (idle timeout configurable)60 seconds origin read timeout (non-configurable)
Certificate propagation after renewalImmediate — ALB picks up renewed cert automaticallyUp to 15 minutes for global edge propagation
Cost modelHourly ALB + LCU for processed bytes/connectionsPer-request pricing + data transfer from origin
Right for MCP ifStandard SSE or WebSocket MCP transportGlobal users, long-lived cached static assets, DDoS risk

Zero-downtime certificate rotation on ALB

Manual certificate rotation — when migrating from RSA to ECDSA, adding a new SAN, or updating a wildcard — follows a three-step pattern that keeps the ALB serving traffic continuously through the change:

# Step 1: Request and validate new certificate
NEW_CERT_ARN=$(aws acm request-certificate \
  --domain-name api.mcp.example.com \
  --subject-alternative-names "*.mcp.example.com" \
  --validation-method DNS \
  --key-algorithm EC_prime256v1 \
  --query 'CertificateArn' --output text)
aws acm wait certificate-validated --certificate-arn $NEW_CERT_ARN

# Step 2: Add new certificate as additional SNI (both old and new active)
LISTENER_ARN=$(aws elbv2 describe-listeners \
  --load-balancer-arn $ALB_ARN \
  --query 'Listeners[?Protocol==`HTTPS`].ListenerArn' --output text)

aws elbv2 add-listener-certificates \
  --listener-arn $LISTENER_ARN \
  --certificates CertificateArn=$NEW_CERT_ARN

# Step 3: Promote new certificate to default
aws elbv2 modify-listener \
  --listener-arn $LISTENER_ARN \
  --certificates CertificateArn=$NEW_CERT_ARN

# Step 4: Remove old certificate (after confirming no clients rejected new cert)
aws elbv2 remove-listener-certificates \
  --listener-arn $LISTENER_ARN \
  --certificates CertificateArn=arn:aws:acm:us-east-1:123456789012:certificate/old-cert

During steps 2 and 3, both certificates are active simultaneously — clients receiving the old certificate via an in-flight connection are not interrupted. ACM certificates are free to keep even after detaching from the ALB, so there is no urgency to delete the old certificate; wait at least 24 hours after the rotation before deleting to ensure any long-lived connections have fully transitioned.

Pattern 3 — Private CA for Internal mTLS

When to use Private CA instead of public ACM

ACM public certificates cover all external HTTPS traffic — any MCP client connecting over the internet gets a publicly trusted certificate at zero cost. Private CA adds cost ($400/month per active CA) and operational overhead (CA hierarchy management, certificate distribution to clients). The correct trigger for adding Private CA is a specific architectural requirement that public ACM cannot satisfy, not a general "more security is better" decision.

RequirementPublic ACMACM Private CA
External HTTPS from internet (ALB, CloudFront)Yes — free, auto-renewed, publicly trustedNo — private CA not trusted by internet clients
mTLS: client presents certificate to serverNo — cannot export certificatesYes — issue client certificates to services
TLS directly on EC2 or containers (without ALB)No — cannot exportYes — exportable
Service-to-service authentication by certificate identityNoYes — CN field identifies the calling service
Custom certificate validity (hours to years)No — 13 months fixedYes — 1 second to 30 years
On-premises or hybrid cloud integrationNoYes

The most common valid use case for Private CA in MCP deployments is when an MCP gateway service must authenticate that incoming connections come specifically from authorized MCP orchestrators — not just any HTTPS client with a valid API key. Mutual TLS achieves this at the transport layer, before the application processes the request, and the ALB can enforce it without requiring any code change in the MCP server.

Building the CA hierarchy

A two-tier hierarchy (ROOT CA + SUBORDINATE CA) is the correct approach. The root CA signs only the subordinate CA certificate, then gets disabled — its private key is less exposed because it is inactive. The subordinate CA issues all end-entity certificates. If the subordinate CA is ever compromised, you create a new subordinate CA (still signed by the offline root) and revoke the old one, without rebuilding from scratch.

# Create root CA (RSA 4096 for the CA key; end-entity certs use EC P-256)
ROOT_CA_ARN=$(aws acm-pca create-certificate-authority \
  --certificate-authority-configuration '{
    "KeyAlgorithm": "RSA_4096",
    "SigningAlgorithm": "SHA512WITHRSA",
    "Subject": {
      "Country": "US",
      "Organization": "MCP Server Internal CA",
      "CommonName": "MCP Server Root CA"
    }
  }' \
  --certificate-authority-type ROOT \
  --query 'CertificateAuthorityArn' --output text)

# Self-sign the root CA certificate (10 year validity)
aws acm-pca get-certificate-authority-csr \
  --certificate-authority-arn $ROOT_CA_ARN \
  --output text > root_ca.csr

ROOT_CERT_ARN=$(aws acm-pca issue-certificate \
  --certificate-authority-arn $ROOT_CA_ARN \
  --csr fileb://root_ca.csr \
  --signing-algorithm SHA512WITHRSA \
  --template-arn arn:aws:acm-pca:::template/RootCACertificate/V1 \
  --validity Value=3650,Type=DAYS \
  --query 'CertificateArn' --output text)

aws acm-pca wait certificate-issued \
  --certificate-authority-arn $ROOT_CA_ARN \
  --certificate-arn $ROOT_CERT_ARN

aws acm-pca get-certificate \
  --certificate-authority-arn $ROOT_CA_ARN \
  --certificate-arn $ROOT_CERT_ARN \
  --query 'Certificate' --output text > root_cert.pem

aws acm-pca import-certificate-authority-certificate \
  --certificate-authority-arn $ROOT_CA_ARN \
  --certificate fileb://root_cert.pem

# Create subordinate CA (EC P-256; signed by root)
SUB_CA_ARN=$(aws acm-pca create-certificate-authority \
  --certificate-authority-configuration '{
    "KeyAlgorithm": "EC_prime256v1",
    "SigningAlgorithm": "SHA256WITHECDSA",
    "Subject": {
      "CommonName": "MCP Server Intermediate CA"
    }
  }' \
  --revocation-configuration '{
    "CrlConfiguration": {
      "Enabled": true, "ExpirationInDays": 7,
      "S3BucketName": "mcp-ca-crls-123456789012",
      "S3ObjectAcl": "BUCKET_OWNER_FULL_CONTROL"
    }
  }' \
  --certificate-authority-type SUBORDINATE \
  --query 'CertificateAuthorityArn' --output text)

# After subordinate CA is active, disable the root CA (saves $400/month)
aws acm-pca update-certificate-authority \
  --certificate-authority-arn $ROOT_CA_ARN \
  --status DISABLED

Short-lived certificates for ECS tasks: the sidecar pattern

For MCP server containers on ECS, the right pattern is a sidecar certificate provisioner that generates a fresh key pair and certificate at task startup. The certificate is valid for 4–24 hours — long enough for the task lifetime, short enough that revocation is moot (the cert expires before a CRL propagates). The private key never leaves the task; only the certificate signing request (CSR) goes to Private CA.

# ECS task definition with certificate provisioner sidecar
{
  "containerDefinitions": [
    {
      "name": "cert-provisioner",
      "image": "public.ecr.aws/amazonlinux/amazonlinux:2023",
      "essential": false,
      "command": ["/bin/bash", "-c",
        "openssl ecparam -name prime256v1 -genkey -noout -out /certs/service.key && \
         openssl req -new -key /certs/service.key \
           -subj \"/CN=${TASK_ARN}/O=MCPServer\" \
           -out /tmp/service.csr && \
         CERT_ARN=$(aws acm-pca issue-certificate \
           --certificate-authority-arn $CA_ARN \
           --csr fileb:///tmp/service.csr \
           --signing-algorithm SHA256WITHECDSA \
           --validity Value=4,Type=HOURS \
           --query CertificateArn --output text) && \
         aws acm-pca wait certificate-issued \
           --certificate-authority-arn $CA_ARN \
           --certificate-arn $CERT_ARN && \
         aws acm-pca get-certificate \
           --certificate-authority-arn $CA_ARN \
           --certificate-arn $CERT_ARN \
           --query Certificate --output text > /certs/service.crt"
      ],
      "mountPoints": [{"sourceVolume": "certs", "containerPath": "/certs"}],
      "environment": [{"name": "CA_ARN", "value": "$SUB_CA_ARN"}]
    },
    {
      "name": "mcp-server",
      "essential": true,
      "dependsOn": [{"containerName": "cert-provisioner", "condition": "SUCCESS"}],
      "mountPoints": [{"sourceVolume": "certs", "containerPath": "/certs", "readOnly": true}]
    }
  ],
  "volumes": [{"name": "certs"}]
}

Enabling mTLS on an ALB

With a Private CA hierarchy and client certificates issued to MCP orchestrators, the ALB can enforce mutual authentication at the network layer. Requests without a valid client certificate receive a 400 response from the ALB — the MCP server backend never sees unauthenticated connections. The client certificate's CN and issuer are forwarded to the backend in HTTP headers for fine-grained authorization.

# Create an ALB trust store containing the CA bundle
aws acm-pca get-certificate-authority-certificate \
  --certificate-authority-arn $SUB_CA_ARN \
  --query 'Certificate' --output text > sub_ca_cert.pem

cat root_cert.pem sub_ca_cert.pem > ca_bundle.pem
aws s3 cp ca_bundle.pem s3://mcp-alb-trust-store/ca-bundle.pem

TRUST_STORE_ARN=$(aws elbv2 create-trust-store \
  --name mcp-client-trust-store \
  --ca-certificates-bundle-s3-bucket mcp-alb-trust-store \
  --ca-certificates-bundle-s3-key ca-bundle.pem \
  --query 'TrustStores[0].TrustStoreArn' --output text)

# Enable mTLS verification on the HTTPS listener
aws elbv2 modify-listener \
  --listener-arn $LISTENER_ARN \
  --mutual-authentication \
    Mode=verify,TrustStoreArn=$TRUST_STORE_ARN,IgnoreClientCertificateExpiry=false

# The ALB now forwards client certificate details to the MCP server backend:
# X-Amzn-Mtls-Clientcert-Subject: "CN=mcp-orchestrator-prod,O=MCPServer"
# X-Amzn-Mtls-Clientcert-Issuer: "CN=MCP Server Intermediate CA"
# X-Amzn-Mtls-Clientcert-Serial: "01:23:45:67:89:ab"
# X-Amzn-Mtls-Clientcert-Validity: "NotBefore=...;NotAfter=..."
# Backend can implement per-CN authorization without any TLS handling code

Cost management: keeping Private CA affordable

At $400/month per active CA, Private CA can easily double the infrastructure cost for a small MCP deployment. The primary lever is the number of CAs in ACTIVE state. A CA in DISABLED state incurs no monthly charge — it cannot issue certificates, but certificates it has already issued remain valid.

ChargeRateCost optimization
Active CA per month$400/monthDisable root CA immediately after signing subordinate CA; keep only subordinate CA active
Certificate issuance$0.75/1,000 (first 1,000/month free)Use 24-hour validity for ECS tasks instead of 1-hour to reduce issuance volume; at 100 tasks/day restarting once: 3,000 certs/month = $1.50
S3 CRL storageStandard S3 pricingNegligible — CRL files are KB-range

For ECS fleets with frequent task restarts, the certificate issuance cost can exceed the CA fee. At $0.75/1,000 certificates, 10,000 issuances per month costs $7.50 — minor compared to the $400 CA cost. But at 1,000 issuances per day (a large ECS fleet with short task lifetimes and aggressive restarts), the monthly issuance cost reaches $22.50/month. Prefer longer certificate validity periods for stable long-running services; reserve 4-hour certificates for genuinely ephemeral workloads.

Consolidated failure modes reference

SymptomCauseFix
Certificate stays PENDING_VALIDATION beyond 30 minutesDNS CNAME not propagated; wrong CNAME value; validation record added to wrong zone (base vs subdomain delegated zone)Verify CNAME with dig _abc123.mcp.example.com CNAME +short; check both base zone and any delegated subdomain zone
Certificate renewal fails with FAILED statusDNS validation CNAME was deleted during DNS provider migration or zone cleanupRe-add CNAME from describe-certificate DomainValidationOptions.ResourceRecord; renewal retries automatically
ACM certificate not appearing in CloudFront certificate selectorCertificate requested in a region other than us-east-1Request new certificate in us-east-1 explicitly with --region us-east-1
SSE connections dropping after exactly 60 secondsALB default idle timeout is 60 seconds; SSE connections with no data activity treated as idleIncrease idle_timeout.timeout_seconds to 300+; or send SSE keepalive comment : keepalive\n\n every 30 seconds from server side
CloudFront returning 502 Bad Gateway from originALB security group does not allow CloudFront IP ranges; origin read timeout exceeded (max 60s)Add managed prefix list com.amazonaws.global.cloudfront.origin-facing to ALB SG inbound rule; use async pattern for MCP tools exceeding 60 seconds
CloudFront caching tool responses — stale data returnedCache behavior for API path using CachingOptimized instead of CachingDisabledSwitch to CachingDisabled policy (4135ea2d-6df8-44a3-9df3-4b5a84be39ad) for all API path patterns; verify with X-Cache: Miss from cloudfront header on every API response
certificate not trusted from MCP client connecting to Private CA-issued certCA certificate chain not distributed to clients; client trust store does not include the Private CA rootExport CA certificate chain and distribute to all clients; for ALB mTLS, update trust store bundle
mTLS returning 400 for valid client certificateClient certificate CN does not match trust store; certificate revoked; certificate expiredCheck ALB access logs ssl_error_code field; temporarily set IgnoreClientCertificateExpiry=true to isolate expiry vs chain issues; verify certificate chain against trust store CA
Private CA costs higher than expectedECS tasks restarting frequently each issuing a new short-lived certificate; root CA left in ACTIVE stateIncrease certificate validity to 24 hours for stable tasks; disable root CA immediately after signing subordinate CA (saves $400/month)
ALB showing old/expired certificate after ACM renewalACM renewal failed (DNS validation CNAME deleted); ALB continues serving the certificate until deletionCheck describe-certificate RenewalSummary.RenewalStatus; fix DNS validation CNAME; ACM retries renewal automatically once CNAME is restored
Cannot attach ACM certificate to EC2 nginx/Apache directlyACM public certificates are not exportable by designPlace an ALB in front of EC2 (ACM on ALB, HTTP to EC2); or use Let's Encrypt certbot directly on EC2; or use ACM Private CA which supports certificate export
Multi-region ALB: certificate in wrong regionACM certificates are regional; us-east-1 certificate cannot be attached to us-west-2 ALBRequest the same domain certificate in each region; DNS validation CNAME is the same for all regions — add once and all regions validate automatically