🔤 DNS and Domain Names Across the Clouds

How a name becomes an address, who answers for your domain, what each cloud's DNS service does (Route 53, Azure DNS, Cloud DNS, Oracle DNS), private names inside a cloud network, and a zone-file checker that catches the classic mistakes (run for real, like the live lookups)

💡 How this page was made. The live lookups were run for real on 6 October 2026, from this machine, with the dig tool (the site's dig page explains it in depth). They are read-only questions to public DNS, and they show real cloud names. The zone-file checker is a small teaching tool I wrote and ran; its sample zones are made up, and it checks only a handful of well-known mistakes, so it is no substitute for the provider's own validation. What each cloud's DNS service offers is from the providers' documentation, read on 6 October 2026 (Route 53, Azure DNS, Cloud DNS and Oracle's DNS service); nothing was run on a cloud account. One more real finding is in the debugging section: on this network, queries aimed at root servers were answered by something that is not a root server, so I could not show a true "trace" of a lookup, and the page says how to recognize that. Related pages: networking (the DNS addresses reserved in each subnet), IAM, cost control and Terraform.

What DNS does

DNS (the Domain Name System) turns a name into the things a program needs: an address, a mail server, a text note. Everything on the internet that you reach by name goes through it, which is why a DNS mistake looks like "the whole service is down". The pieces:

Real answers from a public domain (as run):

user@lab:~$ dig +noall +answer example.com A
example.com.		252	IN	A	104.20.23.154
example.com.		252	IN	A	172.66.147.243
user@lab:~$ dig +short NS example.com
hera.ns.cloudflare.com.
elliott.ns.cloudflare.com.
user@lab:~$ dig +noall +answer example.com SOA
example.com.		1800	IN	SOA	elliott.ns.cloudflare.com. dns.cloudflare.com. 2416374680 10000 2400 604800 1800

Two roles: the resolver and the authoritative server

You can ask the resolver who is in charge of a name, and the NS answer is that delegation:

user@lab:~$ dig +short NS com | head -3
a.gtld-servers.net.
b.gtld-servers.net.
c.gtld-servers.net.
user@lab:~$ dig +short NS amazon.com
ns-1447.awsdns-52.org.
ns-1707.awsdns-21.co.uk.
ns-264.awsdns-33.com.
ns-521.awsdns-01.net.
user@lab:~$ dig +short NS microsoft.com
ns1-39.azure-dns.com.
ns2-39.azure-dns.net.
ns3-39.azure-dns.org.
ns4-39.azure-dns.info.
user@lab:~$ dig +short NS google.com
ns1.google.com.
ns3.google.com.
ns4.google.com.
ns2.google.com.
user@lab:~$ dig +short NS oracle.com
a11-66.akam.net.
ns1.p201.dns.oraclecloud.net.
ns3.p201.dns.oraclecloud.net.
a1-160.akam.net.
ns2.p201.dns.oraclecloud.net.
ns4.p201.dns.oraclecloud.net.
a13-65.akam.net.
a18-67.akam.net.

TTL: the delay you cannot avoid

An answer is cached for its TTL. Asking twice, five seconds apart (as run):

user@lab:~$ dig +noall +answer example.com A; sleep 5; dig +noall +answer example.com A
example.com.		231	IN	A	104.20.23.154
example.com.		231	IN	A	172.66.147.243
example.com.		226	IN	A	104.20.23.154
example.com.		226	IN	A	172.66.147.243

The TTL counted down from 231 to 226: the second answer came from the cache, with the remaining life. This is the single most important fact for changing DNS: after you change a record, anybody who cached the old one keeps using it until its TTL runs out. So:

Names that the cloud gives you

Cloud services have names too, and DNS is how the cloud steers you. Real lookups of the public endpoints (as run):

user@lab:~$ dig +noall +answer management.azure.com
management.azure.com.	3531	IN	CNAME	management.privatelink.azure.com.
management.privatelink.azure.com. 3531 IN CNAME	arm-frontdoor-edge-geo.trafficmanager.net.
arm-frontdoor-edge-geo.trafficmanager.net. 53 IN CNAME edge-management-azure-com.arm-0001.arm-msedge.net.
edge-management-azure-com.arm-0001.arm-msedge.net. 53 IN CNAME arm-0001.arm-msedge.net.
arm-0001.arm-msedge.net. 53	IN	A	4.150.240.10
user@lab:~$ dig +noall +answer storage.googleapis.com
storage.googleapis.com.	170	IN	A	142.250.98.207
storage.googleapis.com.	170	IN	A	142.251.107.207
storage.googleapis.com.	170	IN	A	74.125.196.207
storage.googleapis.com.	170	IN	A	192.178.219.207
storage.googleapis.com.	170	IN	A	173.194.215.207
user@lab:~$ dig +noall +answer s3.eu-west-1.amazonaws.com
s3.eu-west-1.amazonaws.com. 1	IN	A	3.5.74.107
s3.eu-west-1.amazonaws.com. 1	IN	A	52.92.20.16
s3.eu-west-1.amazonaws.com. 1	IN	A	3.5.66.33
s3.eu-west-1.amazonaws.com. 1	IN	A	3.5.75.77
user@lab:~$ dig +noall +answer objectstorage.us-ashburn-1.oraclecloud.com
objectstorage.us-ashburn-1.oraclecloud.com. 52 IN CNAME	objectstorage.us-ashburn-1.oci.oraclecloud.com.
objectstorage.us-ashburn-1.oci.oraclecloud.com.	892 IN A 134.70.28.1
objectstorage.us-ashburn-1.oci.oraclecloud.com.	892 IN A 134.70.32.1
objectstorage.us-ashburn-1.oci.oraclecloud.com.	892 IN A 134.70.24.1

What each cloud's DNS service does

All four host your zone on their global name servers, so you can manage records with the same account, tools and billing as the rest of your cloud. From the documentation:

Delegation: connecting the registrar to the host

Having a domain at a registrar and its DNS at a cloud is normal. The steps, the same on every cloud:

  1. Create the zone (example.com) in the cloud's DNS service. It gives you name servers.
  2. At the registrar, change the domain's NS records to those name servers (this is "delegation"). Oracle's documentation says exactly this about registrars.
  3. Create the records in the cloud (you can do it first, to prepare).
  4. Wait for the old NS TTL to expire, then check with dig +short NS yourdomain. Until the delegation is switched, the cloud's zone receives no queries.

Private names inside the cloud

Inside your network, machines need to find each other by name, and you do not want those names on the public internet. Each cloud has a built-in resolver reachable from every machine at a reserved address (the networking page lists it: on AWS the DNS server is at the VPC base plus two, on Azure .2 and .3 of each subnet are reserved for it, and Azure's platform also uses the virtual address 168.63.129.16), plus private zones for your own names:

A zone-file checker, run for real

Hosting providers let you upload or write zone files, and typos in them cause outages. A DNS zone in the classic text format (a zone file) has rules that are easy to break. This is a 48-line checker I wrote for the commonest mistakes:

#!/usr/bin/env python3
"""A tiny zone-file sanity checker (teaching tool). Usage: zonecheck.py ORIGIN FILE"""
import sys, ipaddress
RFC1918 = [ipaddress.ip_network(n) for n in ('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16')]
origin = sys.argv[1].rstrip('.') + '.'
records = []                                   # (name, ttl, type, value)
ttl_default = 3600
for line in open(sys.argv[2]):
    line = line.split(';')[0].strip()
    if not line: continue
    if line.startswith('$TTL'): ttl_default = int(line.split()[1]); continue
    if line.startswith('$ORIGIN'): continue
    parts = line.split()
    name = parts[0]; rest = parts[1:]
    ttl = ttl_default
    if rest[0].isdigit(): ttl = int(rest[0]); rest = rest[1:]
    if rest[0] == 'IN': rest = rest[1:]
    records.append((name, ttl, rest[0], ' '.join(rest[1:])))

def fqdn(n): return origin if n == '@' else (n if n.endswith('.') else n + '.' + origin)
findings = []
by_name = {}
for name, ttl, typ, val in records:
    by_name.setdefault(fqdn(name), []).append(typ)
for n, types in by_name.items():
    if 'CNAME' in types and len(types) > 1:
        findings.append(('ERROR', n, 'has a CNAME and also other record types (%s): not allowed' % ', '.join(sorted(set(types) - {'CNAME'}))))
    if n == origin and 'CNAME' in types:
        findings.append(('ERROR', n, 'CNAME at the zone apex'))
for name, ttl, typ, val in records:
    n = fqdn(name)
    if typ in ('CNAME', 'NS', 'MX', 'PTR'):
        target = val.split()[-1]
        if not target.endswith('.') and target.endswith(origin.rstrip('.')):
            findings.append(('ERROR', n, '%s target "%s" lacks the final dot: it becomes %s.%s' % (typ, target, target, origin)))
    if typ == 'A':
        try:
            if any(ipaddress.ip_address(val) in net for net in RFC1918): findings.append(('WARN', n, 'public zone points at a private (RFC 1918) address %s' % val))
        except ValueError: findings.append(('ERROR', n, 'bad IPv4 address "%s"' % val))
    if typ == 'MX' and 'CNAME' in by_name.get(fqdn(val.split()[-1]), []):
        findings.append(('ERROR', n, 'MX points at a CNAME'))
    if ttl < 60: findings.append(('WARN', n, 'TTL %d is very low (more queries, slower answers)' % ttl))
    if ttl > 86400: findings.append(('WARN', n, 'TTL %d is over a day (changes will take that long to spread)' % ttl))
if not any(t == 'SOA' for ts in by_name.values() for t in ts): findings.append(('ERROR', origin, 'no SOA record'))
if not any(t == 'NS' for ts in by_name.values() for t in ts): findings.append(('ERROR', origin, 'no NS records'))
for sev, n, msg in sorted(findings): print('%-5s %-24s %s' % (sev, n, msg))
print('%d records, %d findings' % (len(records), len(findings)))
sys.exit(1 if any(f[0] == 'ERROR' for f in findings) else 0)

And two sample zones (made up). A bad one, with the mistakes that happen in real life:

$TTL 3600
@     IN SOA ns1.example.com. admin.example.com. 2026100601 7200 900 1209600 300
@     IN NS  ns1.example.com.
@     IN NS  ns2.example.com
@     IN CNAME web.example.com.
@     IN MX 10 mail
www   IN CNAME web.example.com
web   300 IN A 10.0.1.20
api   5 IN A 203.0.113.45
mail  IN CNAME web
old   200000 IN A 203.0.113.46

and the corrected one:

$TTL 3600
@     IN SOA ns1.example.com. admin.example.com. 2026100601 7200 900 1209600 300
@     IN NS  ns1.example.com.
@     IN NS  ns2.example.com.
@     IN A   203.0.113.45
@     IN MX 10 mail.example.com.
www   IN CNAME example.com.
mail  300 IN A 203.0.113.47
api   300 IN A 203.0.113.48

Both checked (as run):

user@lab:~$ python3 zonecheck.py example.com bad.zone
ERROR example.com.             CNAME at the zone apex
ERROR example.com.             MX points at a CNAME
ERROR example.com.             NS target "ns2.example.com" lacks the final dot: it becomes ns2.example.com.example.com.
ERROR example.com.             has a CNAME and also other record types (MX, NS, SOA): not allowed
ERROR www.example.com.         CNAME target "web.example.com" lacks the final dot: it becomes web.example.com.example.com.
WARN  api.example.com.         TTL 5 is very low (more queries, slower answers)
WARN  old.example.com.         TTL 200000 is over a day (changes will take that long to spread)
WARN  web.example.com.         public zone points at a private (RFC 1918) address 10.0.1.20
10 records, 8 findings
exit status: 1
user@lab:~$ python3 zonecheck.py example.com good.zone
8 records, 0 findings
exit status: 0

What the checker found in the bad zone:

Real tools do far more (the clouds validate on upload, and tools such as named-checkzone check syntax thoroughly), but the rules above are the ones people trip over.

Debugging DNS

A method, in order. The dig page of the Sysadmin tab has the full toolbox:

  1. Does the name resolve at all? dig +short NAME (and dig +noall +answer NAME to see the TTL and any CNAME chain).
  2. Ask more than one resolver. dig +short NAME @1.1.1.1 and @8.8.8.8 against your own: different answers mean caches at different ages, or a resolver that is blocking or rewriting the name.
  3. Ask the authoritative server directly. Find it with dig +short NS DOMAIN, then dig NAME @THAT-SERVER. If the authoritative server is right and the resolver is wrong, the cause is cache, so wait for the TTL (or flush if you control it). If the authoritative server is wrong, fix the zone.
  4. Check delegation: does dig +short NS DOMAIN return the name servers you expect (the cloud's), or the old ones?
  5. Check the private side: from a machine inside the network, dig NAME with no server (the cloud resolver) and with the reserved resolver address.
  6. Look at the SOA serial at each authoritative server: if they differ, a secondary has not caught up.

When the network lies: DNS interception

On this machine I tried to query a root server directly, asking it not to recurse. A real root server answers with a referral (and has no recursion). What came back (as run):

user@lab:~$ dig +norecurse +noall +comments example.com @a.root-servers.net | grep flags
;; flags: qr ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

The flags include ra (recursion available) and the answer section was filled in with the final addresses: a root server would never do that. Something on this network intercepts DNS traffic (a transparent proxy, a security product or a VPN) and answers on behalf of the servers I addressed. So a dig +trace, which is meant to walk the delegation from the root down, could not show the real path from here. This matters in practice: if dig @SERVER always gives the same answer whatever the server, or a root server claims to recurse, a middlebox is answering, and the cloud's DNS is not the only thing involved.

Common mistakes

What this page did not cover

DNSSEC, DNS over HTTPS and TLS, content delivery networks, the details of e-mail records (SPF, DKIM, DMARC), geographic and weighted routing in depth, and registering domains (prices and transfers). Each cloud's DNS documentation, and the Sysadmin tab's dig and Postfix pages, go further.

The cheat sheet

← Cloud Security Baseline: A Checklist You Can Audit · ↑ All tutorials · Serverless Functions on Every Cloud: Handlers, Events and Limits →