Guide · AWS ACM · CloudFront

ACM Certificates with CloudFront for MCP Servers — Edge TLS and CDN

CloudFront is the right TLS termination layer when your MCP server needs low-latency global access, DDoS protection via AWS Shield Standard (included free with CloudFront), or when the origin is an S3 bucket that serves MCP tool static assets. The critical constraint: ACM certificates for CloudFront must be requested in us-east-1 regardless of where your origin is located — this is a hard requirement from CloudFront's global control plane, not a best practice. Request the certificate in the wrong region and it will not appear in the CloudFront distribution certificate selector. This guide covers the certificate request, viewer and origin protocol policy configuration, and cache behavior for MCP server API traffic.

TL;DR

Request the ACM certificate in us-east-1 (mandatory). Set viewer protocol policy to redirect-to-https. Set origin protocol policy to https-only if origin is an ALB with HTTPS, or http-only if origin terminates at HTTP internally. Set minimum TLS version to TLSv1.2_2021. For the certificate overview see ACM overview. For ALB-based HTTPS origins see ACM with ALB.

The us-east-1 requirement

CloudFront is a global service — its control plane operates from us-east-1. When a CloudFront distribution references an ACM certificate, the certificate must be in us-east-1 because that is the only region CloudFront's control plane checks. If you request a certificate for mcp.example.com in us-west-2 and then try to attach it to a CloudFront distribution, it will not appear in the Certificate list — there is no workaround except requesting a new certificate in us-east-1.

# Always specify --region us-east-1 for CloudFront certificates
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 for CloudFront — do not omit

# Wait for validation
aws acm wait certificate-validated \
  --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/abc-123 \
  --region us-east-1

# Note: if your MCP server also uses an ALB in us-west-2, you need a SEPARATE certificate
# in us-west-2 for the ALB. The same domain can have certificates in multiple regions.
aws acm request-certificate \
  --domain-name mcp.example.com \
  --validation-method DNS \
  --key-algorithm EC_prime256v1 \
  --region us-west-2    # For the ALB in us-west-2

# DNS validation CNAME is the same for both regions —
# add it once and both certificates validate automatically

Create a CloudFront distribution with ACM certificate

CloudFront distributions have two protocol layers: the viewer protocol (between client and CloudFront edge) and the origin protocol (between CloudFront edge and your MCP server origin). Configure each independently based on your architecture.

# Create a CloudFront distribution for an MCP server backed by an ALB origin
aws cloudfront create-distribution \
  --distribution-config '{
    "CallerReference": "mcp-server-'$(date +%s)'",
    "Comment": "MCP server distribution",
    "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,
      "FunctionAssociations": { "Quantity": 0 }
    },
    "Origins": {
      "Quantity": 1,
      "Items": [{
        "Id": "mcp-server-alb",
        "DomainName": "mcp-server-alb-123456789.us-east-1.elb.amazonaws.com",
        "CustomOriginConfig": {
          "HTTPPort": 80,
          "HTTPSPort": 443,
          "OriginProtocolPolicy": "https-only",
          "OriginSSLProtocols": { "Quantity": 1, "Items": ["TLSv1.2"] },
          "OriginReadTimeout": 60,
          "OriginKeepaliveTimeout": 5
        },
        "ConnectionAttempts": 3,
        "ConnectionTimeout": 10
      }]
    },
    "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",
    "PriceClass": "PriceClass_100",
    "IsIPV6Enabled": true
  }'

Key settings explained:

Viewer and origin protocol policies

SettingOptionsRecommended for MCP servers
ViewerProtocolPolicyallow-all, redirect-to-https, https-onlyredirect-to-https — clients using HTTP are redirected; https-only if you want to reject HTTP with 403
OriginProtocolPolicyhttp-only, https-only, match-viewerhttps-only when origin is ALB with HTTPS; http-only when origin is internal ECS service on HTTP (traffic never leaves AWS network); avoid match-viewer — it uses HTTP when viewer uses HTTP, bypassing origin TLS
MinimumProtocolVersionSSLv3 through TLSv1.2_2021TLSv1.2_2021 — excludes all deprecated protocols and weak ciphers
OriginSSLProtocolsSSLv3, TLSv1, TLSv1.1, TLSv1.2TLSv1.2 only — when using https-only origin protocol
# Update viewer protocol policy and TLS version on existing distribution
DIST_ID=E1A2B3C4D5E6F7
ETAG=$(aws cloudfront get-distribution-config --id $DIST_ID --query 'ETag' --output text)

# Fetch current config, modify, and update
aws cloudfront get-distribution-config --id $DIST_ID \
  --query 'DistributionConfig' > /tmp/dist-config.json

# Edit the JSON to change MinimumProtocolVersion and ViewerProtocolPolicy
# Then update:
aws cloudfront update-distribution \
  --id $DIST_ID \
  --if-match $ETAG \
  --distribution-config file:///tmp/dist-config.json

# Wait for distribution to deploy (~15 minutes)
aws cloudfront wait distribution-deployed --id $DIST_ID
echo "Distribution updated and deployed globally"

Cache behavior for MCP server API traffic

MCP server API endpoints (tool calls, message exchanges) must not be cached — caching a tool response and serving it to multiple clients is a correctness bug. Configure the cache policy and origin request policy to pass all headers and disable caching for API paths.

# Use the managed CachingDisabled policy for MCP API paths
# CachingDisabled policy ID: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
# This sets TTL min=max=default=0; all requests pass to origin

# Use the AllViewer origin request policy to forward all headers, cookies, and query strings
# AllViewer policy ID: 216adef6-5c7f-47e4-b989-5492eafa07d3

# Add a separate cache behavior for static MCP assets (icons, spec files) with caching enabled
aws cloudfront update-distribution --id $DIST_ID \
  --if-match $(aws cloudfront get-distribution-config --id $DIST_ID --query 'ETag' --output text) \
  --distribution-config '{
    ...existing config...,
    "CacheBehaviors": {
      "Quantity": 1,
      "Items": [{
        "PathPattern": "/static/*",
        "TargetOriginId": "mcp-server-alb",
        "ViewerProtocolPolicy": "redirect-to-https",
        "AllowedMethods": { "Quantity": 2, "Items": ["GET","HEAD"] },
        "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
        "Compress": true
      }]
    }
  }'

# Managed cache policy IDs (no charge to use):
# CachingDisabled:    4135ea2d-6df8-44a3-9df3-4b5a84be39ad  (TTL=0, for API)
# CachingOptimized:   658327ea-f89d-4fab-a63d-7e88639e58f6  (TTL=86400, for static assets)
# CachingOptimizedForCompressedObjects: b2884449-e4de-46a7-ac36-70bc7f1ddd6d

SSE and WebSocket support via CloudFront

CloudFront supports Server-Sent Events (SSE) and WebSockets — both transport protocols used by MCP servers. SSE requires the origin response to use chunked transfer encoding and the Content-Type: text/event-stream header. CloudFront passes these through correctly when caching is disabled (TTL=0) and the AllViewer origin request policy forwards the Accept header.

# For WebSocket connections via CloudFront:
# WebSocket upgrade is handled automatically if:
# 1. CachingDisabled policy is used (TTL=0)
# 2. AllViewer origin request policy is used (forwards Upgrade and Connection headers)
# 3. Origin protocol policy is https-only with wss:// origin
# No special CloudFront configuration is needed for WebSocket beyond these settings.

# For SSE connections via CloudFront:
# SSE streams can be long-lived — CloudFront has a default response timeout of 60 seconds.
# For MCP tools with streaming responses lasting > 60 seconds:
# Increase OriginResponseTimeout (not configurable in standard distributions;
# requires a CloudFront custom configuration — contact AWS Support for long-timeout origins).
# Alternative: use heartbeat comments in SSE stream to keep connection alive within 60s.

The maximum origin read timeout for CloudFront is 60 seconds. For MCP server tools that may run longer than 60 seconds (e.g., code execution tools, long file processing), use an asynchronous pattern: the initial call returns a task ID immediately, and the client polls a separate endpoint for the result. This keeps individual HTTP transactions under 60 seconds while supporting arbitrarily long tool operations.

Route 53 + CloudFront: alias record setup

# Create a Route 53 alias record pointing to the CloudFront distribution
DIST_DOMAIN=$(aws cloudfront get-distribution \
  --id $DIST_ID \
  --query 'Distribution.DomainName' --output text)
# e.g.: d1a2b3c4e5f6g7.cloudfront.net

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

aws route53 change-resource-record-sets \
  --hosted-zone-id $HOSTED_ZONE_ID \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "mcp.example.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "'"$DIST_DOMAIN"'",
          "EvaluateTargetHealth": false
        }
      }
    }, {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "mcp.example.com",
        "Type": "AAAA",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "'"$DIST_DOMAIN"'",
          "EvaluateTargetHealth": false
        }
      }
    }]
  }'
# Z2FDTNDATAQYW2 is the fixed hosted zone ID for all CloudFront distributions

Always create both A (IPv4) and AAAA (IPv6) alias records to leverage CloudFront's dual-stack endpoints. MCP clients connecting over IPv6 will use AAAA, which often results in lower latency on modern networks. The CloudFront hosted zone ID (Z2FDTNDATAQYW2) is constant — it does not change per distribution or per account.

Failure modes reference

SymptomCauseFix
ACM certificate not appearing in CloudFront console certificate selectorCertificate was requested in a region other than us-east-1Request a new certificate in us-east-1 explicitly; ACM certificates from other regions are invisible to CloudFront
CloudFront returning 502 Bad Gateway from originOrigin ALB security group does not allow inbound from CloudFront IP ranges; origin connection timeout; origin read timeout exceededAdd CloudFront IP prefix list to ALB security group (managed prefix list com.amazonaws.global.cloudfront.origin-facing); increase OriginReadTimeout if origin is slow
SSL certificate error: certificate name mismatchCustom domain in CloudFront Aliases list does not match any SAN in the attached ACM certificateAdd the domain to ACM certificate SANs (requires new certificate) or update ACM cert to include the alias domain
CloudFront caching API responses — stale data returned to MCP clientsCache behavior for API path using CachingOptimized instead of CachingDisabledSwitch to CachingDisabled policy for all API path patterns; verify with X-Cache: Miss from cloudfront header (should always be Miss for API endpoints)
SSE stream cuts off mid-responseCloudFront origin read timeout (60s) exceeded; or CloudFront caching buffering the full response before forwardingSend SSE keepalive comments every 30 seconds; ensure CachingDisabled policy is applied to SSE endpoints (not CachingOptimized which buffers)
Distribution still serving old certificate after renewalCloudFront certificate propagation takes up to 15 minutes after ACM renewal; CDN edge nodes update asynchronouslyWait 15-30 minutes; CloudFront pulls the renewed certificate from ACM automatically during normal edge refresh cycles