How to Make CloudFront Serve Your S3 Website with a Custom Domain and HTTPS

You have a static site sitting in S3, a CloudFront distribution in front of it, and a domain registered in Route 53 — but the moment you try to attach a custom domain with HTTPS, the process branches into ACM certificate validation, CloudFront alternate domain names, and Route 53 alias records, all of which have to be wired together in the right order or the whole thing silently fails.

TL;DR: Custom Domain + HTTPS on CloudFront

StepWhat You DoWhy It Matters
1Request ACM certificate in us-east-1CloudFront only reads ACM certs from us-east-1, regardless of your origin region
2Add CNAME validation record to Route 53ACM must verify domain ownership before issuing the cert
3Attach the issued cert to CloudFront and add the alternate domain name (CNAME)CloudFront will reject HTTPS requests for domains not listed as CNAMEs
4Point Route 53 alias record to the CloudFront distributionAlias records resolve the CloudFront domain at the DNS layer without extra latency

How CloudFront Custom Domains and ACM Work Together

CloudFront is a global service, but it has one hard constraint on TLS certificates: it only accepts certificates issued by ACM in the us-east-1 region. This catches engineers off guard when their S3 bucket and other infrastructure live in a different region. The certificate region is non-negotiable — request it anywhere else and CloudFront will not list it as an option.

When a browser connects to your custom domain, Route 53 resolves it to the CloudFront distribution via an alias record. CloudFront then presents the ACM certificate for TLS negotiation. For this to work, the domain on the certificate must match the alternate domain name configured on the distribution. If either side is missing, CloudFront returns an SSL error or a 'Your connection is not private' warning before the request ever reaches your S3 origin.

sequenceDiagram participant Browser participant Route53 as Route 53 participant CF as CloudFront participant ACM participant S3 Note over ACM,Route53: Before go-live: Certificate Validation ACM->>Route53: Requires CNAME validation record Route53-->>ACM: CNAME record detected → Certificate ISSUED Note over Browser,S3: Live request flow Browser->>Route53: DNS query for www.example.com Route53-->>Browser: Alias → d111111abcdef8.cloudfront.net Browser->>CF: HTTPS request (TLS handshake) CF-->>Browser: Presents ACM certificate (us-east-1) Browser->>CF: GET /index.html CF->>S3: Forward request to origin S3-->>CF: 200 OK + content CF-->>Browser: Cached or forwarded response
  1. Browser resolves www.example.com via Route 53 alias → gets the CloudFront distribution domain.
  2. CloudFront presents the ACM certificate (issued in us-east-1) during TLS handshake.
  3. After TLS, CloudFront forwards the request to the S3 origin (either REST endpoint or OAC-protected bucket).
  4. ACM validates domain ownership via a CNAME record you add to Route 53 before the certificate is issued.

Step 1: Request an ACM Certificate in us-east-1

This step must happen in us-east-1 — not your default region, not where your S3 bucket lives. The AWS CLI --region flag is mandatory here. If you skip it and your default profile points to another region, you will request a valid certificate that CloudFront cannot see.

aws acm request-certificate \
  --domain-name example.com \
  --subject-alternative-names www.example.com \
  --validation-method DNS \
  --region us-east-1

The command returns a CertificateArn. Save it — you will need it in Step 3. The certificate status will be PENDING_VALIDATION until you complete Step 2.

Use --subject-alternative-names to cover both the apex domain and the www subdomain in a single certificate. ACM will generate a separate CNAME validation record for each name.

Step 2: Validate the Certificate via DNS (Route 53)

ACM DNS validation works by requiring you to add a CNAME record to your hosted zone. ACM polls for that record and, once it detects it, issues the certificate. This is where most people get stuck — the validation record name is a long, ACM-generated token, not something predictable like _acme-challenge.

First, retrieve the validation CNAME values ACM generated for your certificate:

aws acm describe-certificate \
  --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
  --region us-east-1 \
  --query 'Certificate.DomainValidationOptions'

The output will contain one entry per domain name. Each entry includes a ResourceRecord block with a Name, Type (CNAME), and Value. The Name field looks something like _3e5b6a7c8d9f0a1b2c3d4e5f6a7b8c9d.example.com. — it is unique to your certificate and account. Copy both the Name and Value exactly as returned; do not guess or reconstruct them.

Get your Route 53 hosted zone ID:

aws route53 list-hosted-zones-by-name \
  --dns-name example.com \
  --query 'HostedZones[0].Id' \
  --output text

Then create the validation record. Build a change batch file using the exact values from describe-certificate:

🔽 Click to expand: validation-change-batch.json
{
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.example.com.",
        "Type": "CNAME",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy.acm-validations.aws."
          }
        ]
      }
    }
  ]
}

Important: The Name value above (_xxxxxxxx...example.com.) is a placeholder. You must replace it — and the Value field — with the exact strings returned by aws acm describe-certificate. These tokens are unique per certificate and cannot be derived from the domain name alone.

aws route53 change-resource-record-sets \
  --hosted-zone-id Z1D633PJN98FT9 \
  --change-batch file://validation-change-batch.json

ACM typically detects the record and transitions the certificate to ISSUED within a few minutes, though DNS propagation can extend this. Poll the status:

aws acm describe-certificate \
  --certificate-arn arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
  --region us-east-1 \
  --query 'Certificate.Status'

Do not proceed to Step 3 until the status is ISSUED. CloudFront will not accept a certificate in PENDING_VALIDATION state.

Step 3: Attach the Certificate to CloudFront and Configure the Alternate Domain Name

Two things must happen together here: the ACM certificate ARN gets attached to the distribution, and the custom domain gets added as an alternate domain name (CNAME). CloudFront enforces a strict rule — you cannot serve HTTPS for a domain unless that domain is listed in the distribution's Aliases (alternate domain names). Miss this and CloudFront will either reject the request or serve the default CloudFront certificate, causing a hostname mismatch error in the browser.

Retrieve your current distribution config and its ETag (required for updates):

aws cloudfront get-distribution-config \
  --id EDFDVBD6EXAMPLE \
  --output json > dist-config.json

The file contains a top-level ETag and a nested DistributionConfig object. You need to edit the DistributionConfig portion — specifically the Aliases and ViewerCertificate blocks — then submit only the DistributionConfig object back with the ETag.

🔽 Click to expand: updated ViewerCertificate and Aliases blocks
"Aliases": {
  "Quantity": 2,
  "Items": [
    "example.com",
    "www.example.com"
  ]
},
"ViewerCertificate": {
  "ACMCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "SSLSupportMethod": "sni-only",
  "MinimumProtocolVersion": "TLSv1.2_2021",
  "CloudFrontDefaultCertificate": false
}

Extract just the DistributionConfig block into a separate file (the update API does not accept the ETag inside the JSON body), then apply the update:

aws cloudfront update-distribution \
  --id EDFDVBD6EXAMPLE \
  --distribution-config file://updated-dist-config.json \
  --if-match YOUR_ETAG_VALUE

The distribution will enter InProgress deployment state. Changes propagate to all edge locations before taking effect — check status with:

aws cloudfront get-distribution \
  --id EDFDVBD6EXAMPLE \
  --query 'Distribution.Status'

Step 4: Create Route 53 Alias Records Pointing to CloudFront

With the certificate attached and the alternate domain names configured, the last piece is DNS. Route 53 alias records are the right tool here — they resolve directly to the CloudFront distribution domain at the DNS layer, support the zone apex (bare domain like example.com where a CNAME is not allowed by DNS spec), and do not incur extra query charges for alias resolution to AWS resources.

The CloudFront hosted zone ID for alias records is a fixed value documented by AWS: Z2FDTNDATAQYW2. This is not your Route 53 hosted zone ID — it is the hosted zone ID that represents CloudFront globally for alias record purposes.

🔽 Click to expand: alias-records-change-batch.json
{
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "example.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net.",
          "EvaluateTargetHealth": false
        }
      }
    },
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "www.example.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net.",
          "EvaluateTargetHealth": false
        }
      }
    }
  ]
}
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1D633PJN98FT9 \
  --change-batch file://alias-records-change-batch.json

Replace d111111abcdef8.cloudfront.net. with your actual distribution domain name, which you can retrieve with:

aws cloudfront get-distribution \
  --id EDFDVBD6EXAMPLE \
  --query 'Distribution.DomainName' \
  --output text

Verifying the Full Chain

Once the CloudFront deployment reaches Deployed status and DNS propagates, verify each layer independently before assuming the setup is correct.

Check that DNS resolves to CloudFront:

dig www.example.com +short

Verify the certificate being served matches your domain:

curl -vI https://www.example.com 2>&1 | grep -E 'subject:|issuer:|SSL connection'

Confirm CloudFront is returning a response (not an S3 direct response):

curl -sI https://www.example.com | grep -i 'x-cache\|server\|via'

A response with X-Cache: Hit from cloudfront or Miss from cloudfront confirms traffic is flowing through the distribution. If you see an S3 error page or a certificate mismatch, work back through the checklist: certificate status, alternate domain name configuration, and alias record target.

A Real Failure Pattern Worth Knowing

The most common failure here looks like this: the certificate shows ISSUED, the CloudFront distribution shows Deployed, but the browser still shows a certificate error. The instinct is to blame DNS propagation and wait. That is usually wrong.

The actual cause in most cases: the alternate domain name was not added to the CloudFront distribution, or it was added with a typo (trailing dot, wrong subdomain). CloudFront will serve its default *.cloudfront.net certificate for any domain not explicitly listed in Aliases, which produces a hostname mismatch against your custom domain — not a 'certificate not found' error, but a 'certificate does not match this hostname' error. The two look similar in a browser but point to completely different fixes.

Think of the CloudFront Aliases list as a bouncer list. The ACM certificate is your ID — valid and government-issued. But if your name is not on the list, the bouncer (CloudFront) still falls back to its default credential, which does not match your domain.

Check the aliases on your distribution before assuming a cert or DNS problem:

aws cloudfront get-distribution-config \
  --id EDFDVBD6EXAMPLE \
  --query 'DistributionConfig.Aliases'

IAM Permissions Required

The operations in this post require permissions across three services. Below is a least-privilege policy covering the actions used:

🔽 Click to expand: IAM policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ACMCertificateManagement",
      "Effect": "Allow",
      "Action": [
        "acm:RequestCertificate",
        "acm:DescribeCertificate",
        "acm:ListCertificates"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CloudFrontDistributionUpdate",
      "Effect": "Allow",
      "Action": [
        "cloudfront:GetDistribution",
        "cloudfront:GetDistributionConfig",
        "cloudfront:UpdateDistribution"
      ],
      "Resource": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
    },
    {
      "Sid": "Route53RecordManagement",
      "Effect": "Allow",
      "Action": [
        "route53:ChangeResourceRecordSets",
        "route53:ListHostedZonesByName",
        "route53:GetChange"
      ],
      "Resource": "*"
    }
  ]
}

Note: acm:RequestCertificate, acm:DescribeCertificate, and the Route 53 actions require Resource: * — the Service Authorization Reference confirms these actions do not support resource-level restrictions in all contexts. Scope the CloudFront actions to the specific distribution ARN.

Wrap-Up and Next Steps for Your CloudFront Custom Domain Setup

At this point your static S3 site is served over HTTPS through CloudFront on your custom domain. The four-step sequence — ACM certificate in us-east-1, DNS validation, CloudFront alternate domain + cert attachment, Route 53 alias records — is the complete path. Each step has a hard dependency on the previous one, so order matters.

From here, consider:

  • Enforcing HTTPS-only by setting the CloudFront viewer protocol policy to redirect-http-to-https or https-only.
  • Locking down the S3 bucket with an Origin Access Control (OAC) policy so the bucket is not publicly accessible directly — only through CloudFront.
  • Enabling CloudFront access logging to an S3 bucket for traffic analysis and debugging.

Official references: CloudFront HTTPS requirements, ACM DNS validation, Route 53 alias records for CloudFront.

Glossary

TermDefinition
ACM (AWS Certificate Manager)AWS service that provisions and manages TLS/SSL certificates. CloudFront requires ACM certificates issued in us-east-1.
Alternate Domain Name (CNAME)A custom domain configured on a CloudFront distribution. CloudFront only serves the attached certificate for domains listed here.
Route 53 Alias RecordA Route 53-specific DNS record type that maps a domain to an AWS resource (like a CloudFront distribution) without a standard CNAME. Supports zone apex.
SNI (Server Name Indication)A TLS extension that allows CloudFront to serve multiple certificates on the same IP. sni-only is the standard SSL support method for CloudFront distributions.
OAC (Origin Access Control)A CloudFront mechanism that restricts S3 bucket access to the CloudFront distribution only, replacing the older Origin Access Identity (OAI).

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table

EC2 SSH Connection Timeout: Which Security Group Rules to Check

Difference Between IAM User and IAM Role: Which One Should Your EC2 Use?