.dev TLD vs Externally Hosted Static Sites
If you host a static site on S3 behind CloudFront and buy your domain from an external registrar like Ionos, Namecheap, or GoDaddy, you might hit an unexpected wall with .dev domains. This post explains what goes wrong and walks through a working setup.
The setup that works for most TLDs
If you host your static site in an S3 Bucket through CloudFront with an external registrar then a common pattern might be:
- Request an SSL certificate in ACM.
- Attach the certificate to your CloudFront distribution and add the alternate domain name(s) as needed. (
www.example.com,example.com) - At your registrar, create a CNAME record for
wwwpointing to the CloudFront distribution domain (d111abc.cloudfront.net). - Set up a domain redirect at your registrar so the apex (
example.com) forwards visitors tohttp://www.example.com.
For a .uk or .com domain this works fine. The browser hits the registrar’s redirect servers over HTTP, gets forwarded to www, and CloudFront handles HTTPS from there.
Why .dev breaks this
The .dev TLD is owned by Google and is on the HSTS preload list built into every major browser. This means browsers will never make a plain HTTP connection to any .dev address. Every request is automatically upgraded to HTTPS before a single packet leaves the browser.
When someone visits example.dev:
- The browser forces HTTPS immediately.
- It tries to establish a TLS connection to the registrar’s redirect servers.
- The registrar doesn’t have (or can’t provision) an SSL certificate for your domain on their redirect infrastructure.
- The TLS handshake fails. The visitor sees
ERR_SSL_PROTOCOL_ERRORor similar.
The redirect never fires because the connection is rejected before any HTTP exchange can happen. The www subdomain may still work (the CNAME goes straight to CloudFront, which has the ACM cert), but the apex domain is broken.
This applies to any HSTS-preloaded TLD (.dev, .app, .page, and others) when the registrar’s redirect only supports HTTP.
The underlying DNS constraint
Even if your registrar offered HTTPS redirects, there is a second problem: you cannot place a CNAME record on an apex domain as per DNS standard restriction (RFC 1034).
So for the apex you’re always dependent on your registrar providing some kind of A-record-based redirect. You can’t point it directly at CloudFront via CNAME.
Simpler alternatives worth checking first
Before moving DNS, check whether your registrar already solves this. If one of these works, you can skip the rest of the article.
- HTTPS domain forwarding - some registrars can redirect the apex over HTTPS (they provision a certificate on their end). If yours supports it, this is the lowest-effort fix - no Route 53 needed.
- ALIAS / ANAME / CNAME flattening - some DNS providers offer a non-standard record type that behaves like a CNAME at the apex. If your registrar’s DNS supports this, you can point the apex directly at CloudFront without changing nameservers.
If neither option is available, Route 53 is the reliable fallback.
The Route 53 approach
AWS Route 53 supports a record type called ALIAS which behaves like a CNAME but is allowed at the zone apex. It resolves directly to the CloudFront distribution’s IPs - no redirect hop, no CNAME restriction, and no need for the registrar to terminate TLS. This is what makes it possible to serve both the apex and www from CloudFront over HTTPS.
You don’t need to transfer the domain. Your registrar stays as the registrar; you only change the nameservers to point at Route 53.
Step 1 — Provision your resources
Here is a simplified CDK stack that creates the hosted zone, a wildcard certificate, a CloudFront distribution, and the DNS records:
from aws_cdk import (
CfnOutput, Fn, Stack,
aws_certificatemanager as acm,
aws_cloudfront as cloudfront,
aws_route53 as route53,
aws_route53_targets as targets,
)
DOMAIN = "example.dev"
class SiteStack(Stack):
def __init__(self, scope, construct_id, **kwargs):
super().__init__(scope, construct_id, **kwargs)
zone = route53.PublicHostedZone(
self, "Zone", zone_name=DOMAIN,
)
cert = acm.Certificate(
self, "Cert",
domain_name=DOMAIN,
subject_alternative_names=[f"*.{DOMAIN}"],
validation=acm.CertificateValidation.from_dns(zone),
)
distribution = cloudfront.Distribution(
self, "CDN",
domain_names=[DOMAIN, f"www.{DOMAIN}"],
certificate=cert,
# ... origin, behaviours, etc.
)
# Apex: A + AAAA ALIAS records
route53.ARecord(
self, "Apex",
zone=zone,
target=route53.RecordTarget.from_alias(
targets.CloudFrontTarget(distribution)
),
)
route53.AaaaRecord(
self, "ApexAAAA",
zone=zone,
target=route53.RecordTarget.from_alias(
targets.CloudFrontTarget(distribution)
),
)
# www: standard CNAME
route53.CnameRecord(
self, "Www",
zone=zone,
record_name="www",
domain_name=distribution.distribution_domain_name,
)
CfnOutput(self, "NS",
value=Fn.join(", ", zone.hosted_zone_name_servers),
)
A few things to note:
- The stack must be deployed to
us-east-1because ACM certificates used by CloudFront must live in that region. Cross-region deployment is an option but for a basic webhosting stack it is easier to deploy all resources into a single region. Route 53 is global and S3 bucket region doesn’t matter behind CloudFront, so there’s no practical downside. - A wildcard certificate (
*.example.dev) does not cover the bare apex. You need the certificate to list bothexample.devand*.example.devas Subject Alternative Names. CertificateValidation.from_dns(zone)tells CDK to automatically create the ACM validation CNAME in the hosted zone.
Step 2 — Update nameservers
Once you deployed the new Public Hosted Zone in Route 53 you can:
- Open the Route 53 console and copy the 4 NS records from the new hosted zone.
- In your registrar’s domain settings, switch to custom nameservers and enter those 4 values.
- Wait for DNS to propagate (usually 5-30 minutes, occasionally longer).
- ACM validates the certificate, and the rest of the deployment completes.
After deployment finishes, both https://example.dev and https://www.example.dev resolve to CloudFront.
ACM validates the certificate by checking that a specific CNAME record is resolvable in the domain’s authoritative DNS. After switching nameservers, Route 53 becomes the authoritative DNS, so the validation record must exist there. CDK handles this automatically when you use CertificateValidation.from_dns(zone).
Step 3 — Migrate the Registrar-provided DNS Records
If your registrar provides email services, those services depend on DNS records. For example: MX for delivery, TXT for SPF, CNAME for DKIM/DMARC, etc.
Moving nameservers to Route 53 means you need to recreate those records there. Email will keep working as long as the records are present.
Tip 1
There can be numerous DNS records in the Registrar’s Zone that we have to migrate to the target Zone. It can be time consuming and error-prone to manually copy them all.
The recommendation is to have a look at your provider’s docs and use their API if they have one.
To pull all the records with their configuration can be as simple as:
curl -s "https://api.your-registrar.com/dns/v1/zones/{ZONE_ID}" \
-H "X-API-Key: $DNS_API_KEY" | jq .
Tip 2
Following engineering best practices, it is recommended to separate the configuration out from the business logic. It is no different here, with the DNS provider’s configuration.
It’s worth storing the records in a YAML file and load them dynamically into your infrastructure as code.
This separates external dependencies from your infrastructure logic and makes it obvious which records belong to the registrar:
# email_records.yaml
mx:
- host_name: mx00.example-registrar.com
priority: 10
- host_name: mx01.example-registrar.com
priority: 10
txt:
- record_name: ""
value: "v=spf1 include:_spf.example-registrar.com ~all"
cname:
- record_name: s1._domainkey
domain_name: s1.dkim.example-registrar.com
- record_name: _dmarc
domain_name: dmarc.example-registrar.com
Then in CDK:
import yaml
from pathlib import Path
records = yaml.safe_load(
Path("email_records.yaml").read_text()
)
route53.MxRecord(self, "Mx", zone=zone, values=[
route53.MxRecordValue(host_name=mx["host_name"], priority=mx["priority"])
for mx in records["mx"]
])
for i, cname in enumerate(records.get("cname", [])):
route53.CnameRecord(self, f"EmailCname{i}",
zone=zone,
record_name=cname["record_name"],
domain_name=cname["domain_name"],
)
Troubleshooting
DNS cache: After switching nameservers, your local resolver may cache stale records. Flush it on macOS with sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder. You can also test against a public resolver to bypass local cache:
dig @8.8.8.8 example.dev A
Query Route 53 directly: To verify your records are correct without waiting for propagation:
dig @ns-123.awsdns-45.com example.dev A
Test CloudFront with a resolved IP: If DNS hasn’t propagated but you want to verify CloudFront is serving correctly:
curl --resolve example.dev:443:1.2.3.4 https://example.dev
Replace 1.2.3.4 with an IP returned by the direct Route 53 query above.
Summary
| TLD type | Apex redirect via registrar | Fix |
|---|---|---|
Standard (.com, .uk, etc.) | Works over HTTP | No action needed |
HSTS-preloaded (.dev, .app, .page) | Fails — browser demands HTTPS | Delegate DNS to Route 53 and use ALIAS records |
The key takeaway: if your domain is on an HSTS-preloaded TLD and your site is behind CloudFront, you need a DNS provider that supports ALIAS (or equivalent) records at the apex. Route 53 is the natural choice in an AWS stack, and it doesn’t require transferring the domain away from your registrar.
Questions or thoughts? Feel free to reach out.
Get in touch →