Guide · AWS ACM · Private CA · mTLS

ACM Private CA for MCP Servers — Internal mTLS and Certificate Authority

ACM Private CA is a managed private certificate authority that issues X.509 certificates for internal workloads — including MCP server components — that need mutual TLS (mTLS), where both sides of a connection present and verify a certificate. Unlike ACM public certificates (which are free and auto-renewed but tied to AWS service endpoints), Private CA certificates can be exported and used anywhere: EC2 instances terminating TLS directly, Kubernetes pods, Lambda functions using custom HTTP clients, and inter-service communication channels that must authenticate callers by certificate rather than by API key. The cost model is $400/month per active CA plus $0.75 per 1,000 certificates issued — significant compared to free public ACM certificates, so Private CA is best reserved for scenarios that genuinely require it: mTLS, on-premises integration, custom certificate lifetimes, or exportable certificates.

TL;DR

Create a ROOT CA + SUBORDINATE CA hierarchy (never issue end-entity certificates directly from the root), export the subordinate CA certificate, configure clients to trust your CA certificate, issue short-lived certificates (24h or less for ephemeral tasks), and use certificate revocation via CRL or OCSP. For public certificates see ACM overview. For ALB/CloudFront TLS see ACM with ALB.

When to use Private CA vs public ACM

RequirementPublic ACMACM Private CA
External HTTPS (clients connect from internet)Yes — free, auto-renewedNo — not publicly trusted
Internal service-to-service TLSNo — cannot export; only works with AWS servicesYes — exportable, works anywhere
Mutual TLS (both sides present certificates)No — cannot export client certificatesYes — issue client certificates to services
TLS on EC2 or containers that terminate TLS directlyNoYes
Custom certificate validity period (e.g., 1 hour)No — 13 months fixedYes — any validity from 1 second to 30 years
On-premises integrationNoYes — export cert and key, use with any TLS library
CostFree$400/month/CA + $0.75/1,000 certs

For most MCP server deployments, public ACM certificates with an ALB or CloudFront handle external TLS at zero cost. Add Private CA only when you specifically need mTLS for service-to-service authentication — for example, when an MCP gateway service must verify that incoming connections come from authenticated MCP clients, not arbitrary HTTP callers.

Create a Private CA hierarchy

Best practice is a two-tier hierarchy: a ROOT CA that signs only the SUBORDINATE CA certificate, and the SUBORDINATE CA that issues end-entity certificates. The root CA's private key is then less exposed — you can keep it disabled (Active=false) between uses. Never issue end-entity certificates directly from the root CA.

# Step 1: Create the root CA (RSA 4096 for CA key; EC_prime256v1 also supported)
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",
      "OrganizationalUnit": "Security",
      "State": "California",
      "Locality": "San Francisco",
      "CommonName": "MCP Server Root CA"
    }
  }' \
  --certificate-authority-type ROOT \
  --tags Key=Service,Value=mcp-server Key=Type,Value=root-ca \
  --query 'CertificateAuthorityArn' --output text)

# Step 2: Get the CSR for the root CA and self-sign it
aws acm-pca get-certificate-authority-csr \
  --certificate-authority-arn $ROOT_CA_ARN \
  --output text > root_ca.csr

# Issue the self-signed root certificate (validity: 10 years)
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

# Step 3: Import the self-signed certificate to activate the root CA
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

# Step 4: Create the subordinate CA
SUB_CA_ARN=$(aws acm-pca create-certificate-authority \
  --certificate-authority-configuration '{
    "KeyAlgorithm": "EC_prime256v1",
    "SigningAlgorithm": "SHA256WITHECDSA",
    "Subject": {
      "Country": "US",
      "Organization": "MCP Server Internal CA",
      "OrganizationalUnit": "MCP Services",
      "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)

# Step 5: Sign the subordinate CA with the root CA
aws acm-pca get-certificate-authority-csr \
  --certificate-authority-arn $SUB_CA_ARN \
  --output text > sub_ca.csr

SUB_CERT_ARN=$(aws acm-pca issue-certificate \
  --certificate-authority-arn $ROOT_CA_ARN \
  --csr fileb://sub_ca.csr \
  --signing-algorithm SHA512WITHRSA \
  --template-arn arn:aws:acm-pca:::template/SubordinateCACertificate_PathLen0/V1 \
  --validity Value=5,Type=YEARS \
  --query 'CertificateArn' --output text)

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

# Retrieve cert + chain and activate the subordinate CA
aws acm-pca get-certificate \
  --certificate-authority-arn $ROOT_CA_ARN \
  --certificate-arn $SUB_CERT_ARN \
  --query '{Cert:Certificate,Chain:CertificateChain}' \
  --output json > sub_cert_and_chain.json

aws acm-pca import-certificate-authority-certificate \
  --certificate-authority-arn $SUB_CA_ARN \
  --certificate fileb://<(jq -r '.Cert' sub_cert_and_chain.json) \
  --certificate-chain fileb://<(jq -r '.Chain' sub_cert_and_chain.json)

Issue certificates to MCP server components

With the subordinate CA active, you can issue end-entity certificates to MCP server ECS tasks, Lambda functions, or any service that needs a TLS identity. Use short validity periods (hours to days for ephemeral workloads) — Private CA has no forced minimum lifetime, unlike public ACM's 13 months.

# Issue a certificate to an MCP gateway service (valid 24 hours for ephemeral ECS task)
# Step 1: Generate a key pair and CSR in the service (outside of ACM — never share private keys)
openssl ecparam -name prime256v1 -genkey -noout -out service.key
openssl req -new -key service.key \
  -subj "/CN=mcp-gateway.internal/O=MCP Server" \
  -out service.csr

# Step 2: Issue the certificate from Private CA
SERVICE_CERT_ARN=$(aws acm-pca issue-certificate \
  --certificate-authority-arn $SUB_CA_ARN \
  --csr fileb://service.csr \
  --signing-algorithm SHA256WITHECDSA \
  --validity Value=24,Type=HOURS \
  --query 'CertificateArn' --output text)

aws acm-pca wait certificate-issued \
  --certificate-authority-arn $SUB_CA_ARN \
  --certificate-arn $SERVICE_CERT_ARN

# Step 3: Retrieve and store certificate (private key stays on service; only cert is from ACM)
aws acm-pca get-certificate \
  --certificate-authority-arn $SUB_CA_ARN \
  --certificate-arn $SERVICE_CERT_ARN \
  --query 'Certificate' --output text > service.crt

# Step 4: Configure the MCP server to present service.crt + service.key for TLS
# The CA certificate chain (root + subordinate) must be distributed to all clients
# that need to verify this service's identity
# Pattern: issue certificates in ECS task using a sidecar container
# The sidecar requests a certificate at task startup and writes it to a shared volume.
# The MCP server container reads the cert/key from the shared volume and starts serving.

# ECS task definition (relevant snippet):
{
  "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": "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123" }
      ]
    },
    {
      "name": "mcp-server",
      "image": "your-mcp-server-image:latest",
      "essential": true,
      "dependsOn": [{ "containerName": "cert-provisioner", "condition": "SUCCESS" }],
      "mountPoints": [{ "sourceVolume": "certs", "containerPath": "/certs", "readOnly": true }]
    }
  ],
  "volumes": [{ "name": "certs" }]
}

Configuring mTLS on ALB with Private CA

ALB supports mTLS (mutual TLS) where the ALB verifies client certificates against a trust store. This enables the ALB to authenticate MCP clients by certificate before forwarding requests to the backend, without requiring application-layer API key validation.

# Step 1: Create a trust store with the Private CA certificate chain
# Export CA certificate to include in the trust store
aws acm-pca get-certificate-authority-certificate \
  --certificate-authority-arn $SUB_CA_ARN \
  --query 'Certificate' --output text > sub_ca_cert.pem

# Include both root and subordinate CA in the trust store bundle
cat root_cert.pem sub_ca_cert.pem > ca_bundle.pem

# Upload the CA bundle to an S3 bucket
aws s3 cp ca_bundle.pem s3://mcp-alb-trust-store/ca-bundle.pem

# Step 2: Create the ALB trust store
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)

# Step 3: Enable mTLS on the HTTPS listener
LISTENER_ARN=$(aws elbv2 describe-listeners \
  --load-balancer-arn $ALB_ARN \
  --query 'Listeners[?Protocol==`HTTPS`].ListenerArn' --output text)

aws elbv2 modify-listener \
  --listener-arn $LISTENER_ARN \
  --mutual-authentication \
    Mode=verify,TrustStoreArn=$TRUST_STORE_ARN,IgnoreClientCertificateExpiry=false

# Step 4: ALB forwards client cert details to backend in HTTP headers
# X-Amzn-Mtls-Clientcert-Subject: "CN=mcp-client-123,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=2026-10-09T00:00:00Z;NotAfter=2026-10-10T00:00:00Z"
# X-Amzn-Mtls-Clientcert: (full URL-encoded PEM certificate)
# X-Amzn-Mtls-Clientcert-Leaf: (leaf cert only)

Once mTLS is enabled on the ALB listener, the ALB validates the client certificate against the trust store before forwarding the request. Requests without a valid client certificate receive a 400 response at the ALB level — the MCP server backend never sees unauthenticated requests. The backend can access the client certificate details from the X-Amzn-Mtls-Clientcert-* headers to implement fine-grained authorization (e.g., only allow clients with CN=mcp-orchestrator to call specific tool endpoints).

Certificate revocation with CRL

Private CA supports Certificate Revocation Lists (CRL) and OCSP. CRL is more commonly used — it publishes a list of revoked certificate serial numbers to an S3 bucket, which clients check periodically. OCSP provides real-time revocation checking but requires clients to support it.

# Revoke a specific certificate (e.g., compromised service key)
aws acm-pca revoke-certificate \
  --certificate-authority-arn $SUB_CA_ARN \
  --certificate-serial SERIAL_HEX_VALUE \
  --revocation-reason KEY_COMPROMISE
# Revocation reason options:
#   KEY_COMPROMISE, CA_COMPROMISE, AFFILIATION_CHANGED,
#   SUPERSEDED, CESSATION_OF_OPERATION, PRIVILEGE_WITHDRAWN,
#   UNSPECIFIED, A_A_COMPROMISE

# Retrieve the CRL distribution point URL from the CA certificate
aws acm-pca get-certificate-authority-certificate \
  --certificate-authority-arn $SUB_CA_ARN \
  --query 'Certificate' --output text | \
  openssl x509 -noout -text | grep "CRL Distribution"
# Output: URI:http://mcp-ca-crls-123456789012.s3.amazonaws.com/crl.crl

# For short-lived certificates (hours), revocation is less critical —
# the certificate will expire before the CRL is widely propagated anyway.
# Short validity is the preferred revocation strategy for ephemeral workloads.

Pricing and cost management

ChargeRateNotes
Active CA per month$400/monthPer CA in ACTIVE state; disabled CAs are $0; root CA can be disabled when not signing subordinate certs
Certificate issuance (Private CA mode)$0.75/1,000 certsFirst 1,000 certificates/month free; reissuing a cert to the same ECS task on restart counts as a new cert
Certificate issuance (End-entity certificates via AWS services)$0.75/1,000 certsSame rate regardless of whether issued via ACM or direct PCA API
S3 CRL storageStandard S3 pricingCRL files are small (KB range); negligible cost

Cost optimization: keep the root CA disabled — set it to ACTIVE only when signing a new subordinate CA certificate, then disable again. This saves $400/month for the root CA when you have multiple subordinate CAs per account. For ECS deployments with frequent task restarts, use longer certificate validity periods (24 hours instead of 1 hour) to reduce issuance volume — at $0.75/1,000 certs, 1,000 issuances per day = $22.50/month in certificate fees, which may exceed the CA fee itself.

Failure modes reference

SymptomCauseFix
InvalidStateException when issuing certificateCA is not in ACTIVE state; CA was just created and certificate has not been imported yetComplete CA activation (import the signed CA certificate); verify CA status with describe-certificate-authority
Client rejects server certificate with "unknown CA"CA certificate chain not distributed to clients; client trust store does not include the Private CA's root certificateExport CA certificate chain and add to client trust store; for ALB trust stores, upload updated CA bundle and refresh
mTLS returning 400 for valid client certificateClient certificate CN does not match trust store; certificate is revoked; certificate has expiredCheck ALB access logs for ssl_error_code field; use IgnoreClientCertificateExpiry=true temporarily to diagnose; verify certificate chain matches trust store CA
High Private CA costs despite few MCP servicesECS tasks restarting frequently each issuing a new certificate; short validity forcing frequent reissuanceIncrease certificate validity period; use ECS task role assumption to cache certificates in SSM Parameter Store for the task's lifetime; consider Let's Encrypt for non-mTLS internal TLS needs
Certificate issuance fails with LimitExceededExceptionRate limit on certificate issuance per CA (default: 25 certs/second)Implement certificate caching; use exponential backoff on issuance; request limit increase via Service Quotas if traffic genuinely requires higher rates