# DNS Records Explained: A, CNAME, MX, TXT, TTL and the Proxy

> DNS records explained: trace a lookup from resolver to authoritative server, learn A, CNAME, MX, TXT, NS and CAA, what TTL caches and what proxying does.

URL: https://www.matthewswong.com/en/learn/devops-from-zero/dns-records-explained/

DevOps from zero · Lesson 17 of 25 · Reviewed on 18 October 2026 · By Matthews Wong (https://www.matthewswong.com/en/about/)

> A DNS record is one entry in a domain's zone that answers one question about a name. A and AAAA give its addresses, CNAME makes it an alias of another name, MX names its mail servers, TXT holds text such as SPF, NS names its authoritative servers, and CAA limits who may issue its certificates. Every answer carries a TTL: how long resolvers may cache it.

## You will be able to

- Trace a lookup from your laptop's stub resolver through a recursive resolver to the root, TLD and authoritative servers, and replay it with dig +trace.
- Pick the right record type for a job, and explain why a CNAME cannot sit at the zone apex.
- Tell a proxied name from a DNS-only one with dig, and predict what breaks when SSH or a strict CSP meets the proxy.

## One DNS lookup, then the traffic it sends to the proxy

Your laptop's stub resolver asks a recursive resolver, which follows referrals from a root server and a com server to the zone's authoritative server. The proxied answer sends HTTPS to Cloudflare, while SSH to the same name times out.

1. **Meet the parts.** Top: the servers one name lookup visits. Bottom: where traffic goes once the name has addresses. The name is www.matthewswong.com, and Cloudflare proxies it.
2. **Ask a resolver.** Your laptop runs a stub resolver, which cannot do the whole lookup itself. It sends the question, the A record for www.matthewswong.com, to a recursive resolver.

   ```
   dig @one.one.one.one www.matthewswong.com A
   ```
3. **The root refers.** With nothing cached, the resolver starts at a root server. The root does not know the answer. It returns a referral: the name servers for com.

   ```
   dig +trace www.matthewswong.com
   ```
4. **com refers to the zone.** The resolver asks a com server, which refers it on to the zone's own NS records: two Cloudflare name servers, cached for two days.

   ```
   dig matthewswong.com NS +short
   ```

   *Predict:* Cloudflare proxies this name. What does the authoritative server answer? (The origin server's address / Two addresses that belong to Cloudflare / A CNAME that points at the origin)
5. **The authoritative answer.** The authoritative server answers with two A records that belong to Cloudflare, TTL 300. A proxied name never shows the origin's address. The resolver caches the answer for 300 seconds.

   ```
   dig @one.one.one.one www.matthewswong.com +noall +answer
   ```
6. **HTTPS meets the proxy.** The browser connects to those addresses on port 443. The proxy handles the request, passes it to the origin, and can change the response: here it injects an analytics beacon.

   ```
   curl -sI https://www.matthewswong.com/en/
   ```

   *Predict:* A deploy job runs ssh to the same proxied name, on port 22. What happens? (The proxy forwards it to the origin / The proxy redirects it to port 443 / Nothing answers, and the connection times out)
7. **SSH to a proxied name.** By default the proxy carries only HTTP and HTTPS ports. SSH on port 22 reaches Cloudflare's addresses, nothing answers, and the deploy times out. Use a DNS-only name or the address.

   ```
   curl --connect-timeout 5 telnet://www.matthewswong.com:22
   ```

## What is a DNS record?

DNS turns names into data, and a DNS record, a resource record or RR in the RFCs, is one piece of that data. Every record has an owner name, a type, a TTL and a value: www.matthewswong.com, type A, TTL 300, and an address. The records for one domain live together in a zone, and the zone's authoritative name servers are the ones that answer for it.

dig shows exactly those parts. With +noall +answer it prints only the answer section, one record per line: the name, the TTL, the class, the type and the value. That is the view this lesson uses, and the try-it section runs it against this site's own name.

## How does a DNS lookup work, from browser to authoritative server?

Your laptop does not walk the tree itself. It runs a stub resolver, which RFC 9499 defines as a resolver that cannot perform all resolution itself, so it hands the whole question to a recursive resolver: the one listed in /etc/resolv.conf, or a public one such as Cloudflare's, reachable by the name one.one.one.one. In recursive mode, as RFC 1034 puts it, the first server pursues the query for the client at another server.

A recursive resolver with an empty cache starts at the root, the zone whose apex is the zero-length label. A root server does not know the answer; it returns a referral to the servers for com, the top-level domain one layer below the root. A com server returns another referral, the NS records of matthewswong.com. Only then does the resolver reach a server that answers with authority.

dig +trace replays that walk. Its manual describes it as tracing the delegation path from the root name servers and showing the answer from each server it used. Ask an authoritative server directly and its reply carries the aa flag, which RFC 1035 defines as the responding server being an authority for the name asked about. After that it is all cache: the resolver keeps the answer for its TTL and serves the next asker without walking the tree again.

## Which DNS record types matter, and what does each one do?

A holds a host address, an IPv4 one, and AAAA stores a single IPv6 address. CNAME makes a name an alias: its value is the canonical name, and RFC 1034 says the server includes the CNAME in its response and restarts the query at that name. Use A and AAAA when you know the addresses, and CNAME when another name owns them, such as a hosting platform or a CDN.

MX names the mail servers for a domain, each with a preference number, and lower values are preferred. RFC 1912 repeats an older rule that MX records must not point to an alias defined by a CNAME. TXT holds text strings, and other systems keep their own data there: SPF, for example, must be published as a TXT record, and its value starts with v=spf1.

NS lists the authoritative name servers for a zone. RFC 1034 puts NS records at the top node of a zone, and at the cut where a parent zone delegates to a child, which is exactly the referral the com servers hand out. CAA names the certificate authorities allowed to issue certificates for the domain, and RFC 8659 says a compliant CA must check for it before issuing.

## Why can't a CNAME sit at the zone apex?

The apex is the top of a zone, the bare domain such as example.com. RFC 9499 defines it as the owner of an SOA record and the zone's authoritative NS records, so the apex always carries those. RFC 1034 says that if a CNAME is present at a node, no other data should be present, and RFC 1912 puts it more bluntly: a CNAME is not allowed to coexist with any other data. A CNAME at the apex would have to sit beside SOA and NS, which the rule forbids.

DNS providers work around it by resolving the alias themselves. Cloudflare's CNAME flattening returns the final IP addresses instead of a CNAME, and does so by default when the apex uses a CNAME. Amazon Route 53 has alias records, which, unlike a CNAME, can be created at the zone apex, and dig lists them as plain A or AAAA records. Either way, a resolver only ever sees address records.

That is one practical reason sites serve on www. A subdomain can be a plain CNAME to a platform or a CDN at any DNS provider, while the apex needs a provider-specific feature. Every canonical URL on this site is built on www, from one constant in lib/seo.ts.

## What is TTL, and what is DNS propagation really?

TTL is a time limit, in seconds, on how long a record may be kept in a cache. A resolver that fetched www.matthewswong.com with TTL 300 may answer from its cache for up to five minutes, and two dig runs a few seconds apart show the remaining TTL counting down. On Cloudflare, every proxied record has a TTL of Auto, which is 300 seconds and cannot be edited.

So DNS propagation is not a change travelling across the internet. Once the authoritative servers hold the new record, every resolver keeps serving its cached copy until that copy's TTL runs out, so the change appears resolver by resolver. Cloudflare's docs put it plainly: a longer TTL means updates take longer to go into effect. Before a planned change, lower the TTL and wait out the old one, so caches are short-lived when you switch.

Changing name servers is slower still, because the delegation lives in the parent zone. In a dig +trace of this site, the com servers hand out the domain's NS records with a TTL of 172800 seconds, two days.

## What does Cloudflare's proxy, the orange cloud, change?

A record set to Proxied is answered with Cloudflare anycast IP addresses instead of your server's, and its HTTP and HTTPS traffic routes through Cloudflare's network, where caching, WAF and redirect rules apply. Set to DNS only, Cloudflare answers with your server's real address, which, in Cloudflare's words, exposes your origin IP address to anyone who queries the record. Only A, AAAA and CNAME records can be proxied; MX and TXT are always DNS only.

The proxy is a second server in the path, and two consequences show up in this repository. By default it carries only the HTTP and HTTPS ports Cloudflare lists, 80 and 443 among them, so SSH on port 22 to a proxied name goes nowhere. And it can change the response: with Web Analytics' automatic setup, Cloudflare injects a JavaScript beacon into the pages, which this site's Content-Security-Policy had to allow.

## The real config behind this lesson

`next.config.ts:134-145`

```typescript
            // The cloudflareinsights hosts are the Web Analytics beacon the
            // Cloudflare proxy injects into every page since www went behind
            // it; without them the beacon is blocked and logs a CSP error on
            // every page view.
            key: "Content-Security-Policy",
            value: [
              "default-src 'self'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://www.google-analytics.com https://static.cloudflareinsights.com",
              "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
              "img-src 'self' data: blob: https: http:",
              "font-src 'self' https://fonts.gstatic.com",
              "connect-src 'self' https://www.google-analytics.com https://api.groq.com https://cloudflareinsights.com",
```

The proof that a proxy is not a passive pipe. Once www was proxied, Cloudflare started inserting a script the app never wrote, and the app's own CSP blocked it. script-src now allows static.cloudflareinsights.com, where the beacon loads from. Cloudflare's FAQ says the automatic setup reports to the site's own /cdn-cgi/rum, which 'self' covers; cloudflareinsights.com is the endpoint for manual installs.

`lib/seo.ts:5`

```typescript
export const BASE_URL = "https://www.matthewswong.com";
```

One constant decides the site's host name: buildCanonical and toAbsoluteUrl build every canonical URL and sitemap address on it, so the site lives on www. A subdomain like www can be a plain CNAME at any provider; the apex cannot, which is one reason to put the site there.

`.github/workflows/deploy.yml:3-6`

```yaml
# Build a Docker image, push it to GHCR, then deploy it to the VPS over SSH.
# All app configuration is read from GitHub at run time:
#   Secrets  → GROQ_API_KEY, RESEND_API_KEY, VPS_SSH_HOST, VPS_SSH_USER, VPS_SSH_KEY
#   Variables→ CONTACT_FROM_EMAIL, CONTACT_TO_EMAIL, APP_PORT, DEPLOY_PATH, VPS_SSH_PORT
```

The deploy's target is a secret, VPS_SSH_HOST, not a name written in the workflow. That makes it easy to get wrong after a DNS change: if the secret holds a name that is now proxied, the deploy connects to Cloudflare instead of the server. It must hold a DNS-only name or the server's address.

`.github/workflows/deploy.yml:213-219`

```yaml
      - name: Deploy over SSH
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.VPS_SSH_HOST }}
          username: ${{ secrets.VPS_SSH_USER }}
          key: ${{ secrets.VPS_SSH_KEY }}
          port: ${{ vars.VPS_SSH_PORT || 22 }}
```

Where the secret is used. The port falls back to 22, which is not on Cloudflare's list of proxied ports, so a proxied host here never reaches sshd. The step does not fail fast with a refusal: the connection waits and then times out, which looks like an outage rather than a DNS mistake.

## What breaks

- **An SSH deploy times out because its host is a proxied name.** Cloudflare proxies only its listed HTTP and HTTPS ports by default, and 22 is not one of them. A proxied name resolves to Cloudflare's addresses, so ssh connects there, nothing answers, and the step hangs until it times out. Fix it where the host is configured, here the VPS_SSH_HOST secret: use a DNS-only name or the server's address. Cloudflare's own advice for an SSH server is the same: make that name DNS only.
- **The proxy hides nothing if another record names the same server.** A proxied record keeps the origin's address out of that one answer. Any DNS-only record that points at the same machine, such as an admin subdomain or a mail host, publishes it to anyone who queries it, as Cloudflare warns. Check every record in the zone, not just the one you proxied.
- **A CNAME beside other records, or at the apex.** A CNAME cannot share its name with any other record, so you cannot add an MX or a TXT record to a name that is a CNAME, and the apex cannot be a CNAME at all. RFC 1912 adds that a server like BIND sees the CNAME and refuses other data for that name, so NS records beside it are ignored. Point MX at a name with A or AAAA records, never at an alias, and use flattening or an alias record at the apex.
- **You changed the record, but you still get the old answer.** Your resolver cached the old answer and may keep it for the rest of its TTL. Ask one of the zone's name servers directly with dig: if it shows the new value, the change is done and only caches are behind. Cloudflare's public resolver also has a Purge Cache page at one.one.one.one/purge-cache for a single name.
- **A strict CSP blocks what the proxy injected.** Turn the proxy on for a site with a Content-Security-Policy, and the Web Analytics beacon Cloudflare injects is blocked, with a CSP error on every page view. Either allow the beacon's hosts, as this site's next.config.ts does, or change the Web Analytics setting. Cloudflare also notes that a Cache-Control of public, no-transform stops it from modifying the page, so the beacon is not injected at all.

### Proxied or DNS only: what each setting does to a Cloudflare record

| Question | Proxied (orange cloud) | DNS only (grey cloud) |
| --- | --- | --- |
| What dig returns | Cloudflare anycast addresses | Your server's real address |
| Traffic it carries | HTTP and HTTPS on Cloudflare's listed ports | None: clients connect straight to your server |
| Can change responses | Yes: caching, WAF, redirects, an injected beacon | No, the traffic never touches Cloudflare |
| TTL | Auto, 300 seconds, not editable | Your choice, up to 1 day |
| Record types | A, AAAA and CNAME only | Any type; MX and TXT always |
| Use it for | Websites and HTTP APIs | SSH, mail and other non-HTTP services |

## Try it yourself

You need a terminal with dig and curl; macOS ships both, and on Linux dig comes with your distribution's BIND tools. Everything here only reads public answers, so it is safe to run against this site. Compare what you see with the descriptions rather than exact numbers, which change.

1. Ask Cloudflare's public resolver for the A record. You get two addresses that belong to Cloudflare, each with a TTL of 300 or a little less.

   ```bash
   dig @one.one.one.one www.matthewswong.com +noall +answer
   ```
2. Run it again a few seconds later. The TTL is usually lower by about the seconds you waited: that is the resolver's cached copy counting down.

   ```bash
   dig @one.one.one.one www.matthewswong.com +noall +answer
   ```
3. Ask for other types. AAAA returns two IPv6 addresses, also Cloudflare's. NS returns the zone's two Cloudflare name servers. MX returns three mail servers, each with a preference number in front.

   ```bash
   dig @one.one.one.one www.matthewswong.com AAAA +short
   dig @one.one.one.one matthewswong.com NS +short
   dig @one.one.one.one matthewswong.com MX +short
   ```
4. Walk the delegation yourself: the root servers, then the com servers, then the zone's name servers, then the answer. On the network this lesson was tested on, the plain trace looped; adding +tcp made it work.

   ```bash
   dig +trace www.matthewswong.com
   dig +trace +tcp www.matthewswong.com
   ```
5. Ask one of the zone's name servers directly. Look for aa in the flags line: an authoritative answer, with the full TTL of 300. If aa is missing, add +tcp; on the test network, only the TCP query came back with aa.

   ```bash
   dig @$(dig +short matthewswong.com NS | head -1) www.matthewswong.com
   dig +tcp @$(dig +short matthewswong.com NS | head -1) www.matthewswong.com
   ```
6. Now look at the proxy. The server header says cloudflare, and when you ask for HTML the way a browser does, the page contains a beacon script the app never wrote.

   ```bash
   curl -sI https://www.matthewswong.com/en/ | grep -i '^server'
   curl -s -H 'Accept: text/html' https://www.matthewswong.com/en/ | grep -o 'static.cloudflareinsights.com/beacon.min.js'
   ```
7. Finally, try the SSH port. HTTPS connects at once, but port 22 on the same name waits and then fails with a timeout, because the proxy does not carry it.

   ```bash
   curl -sS --connect-timeout 5 -o /dev/null https://www.matthewswong.com/en/ && echo 443 ok
   curl -sS --connect-timeout 5 telnet://www.matthewswong.com:22
   ```

## Quick quiz

1. **Why can't example.com itself be a CNAME?**
   A. CNAMEs only work on names with at least three labels
   B. The apex must hold SOA and NS records, and a CNAME cannot share a name with other data
   C. Browsers refuse to follow a CNAME at the apex
   Answer: B. The apex owns the zone's SOA and NS records, and RFC 1034 says no other data should sit beside a CNAME. Flattening and alias records answer with addresses instead.

2. **You change an A record whose TTL is 3600. Twenty minutes later a colleague still gets the old address. What is happening?**
   A. Their resolver cached the old answer and may keep it for up to an hour
   B. The change has not reached the root servers yet
   C. The authoritative server rejected the new record
   Answer: A. TTL is how long a resolver may cache a record. Root servers never hold your records; they only refer resolvers onward. Ask the zone's name server directly to confirm the change landed.

3. **A deploy step SSHes to deploy.example.com, hangs, then times out. dig shows the name returns two Cloudflare addresses. What is the fix?**
   A. Open port 22 in the server's firewall
   B. Raise the SSH timeout in the workflow
   C. Point the deploy at a DNS-only name or the server's address
   Answer: C. The name is proxied, so ssh reaches Cloudflare, which does not carry port 22 by default. The server is never contacted, so its firewall and a longer timeout change nothing.

## Think it through

- **When the resolver asks a com server about www.matthewswong.com, what comes back?** A referral, not the address: the NS records of matthewswong.com, which name two Cloudflare name servers. The resolver then asks one of those, and that server answers with authority.
- **Why can't you turn on the orange cloud for an MX record?** Cloudflare proxies only the records used for address resolution: A, AAAA and CNAME. MX and TXT are always DNS only, and mail is not traffic on the HTTP and HTTPS ports the proxy carries anyway.
- **What is DNS propagation, in terms of TTL?** Caches expiring. Once the authoritative servers hold the new record, each resolver keeps its cached copy until that copy's TTL runs out, so the change shows up resolver by resolver within one old TTL.

## Go deeper

- [Expose Self-Hosted Services Safely with Cloudflare Tunnel](https://www.matthewswong.com/en/blog/cloudflare-tunnel-zero-trust-homelab/)
- [How a CDN Works: What to Cache at the Edge and What Not To](https://www.matthewswong.com/en/blog/how-cdn-works-edge-caching-explained/)
- [High Availability Design: Redundancy, Failover and the Nines](https://www.matthewswong.com/en/blog/high-availability-redundancy-failover-design/)
- [How to Set Up Nginx Reverse Proxy for Docker and Cloudflare](https://www.matthewswong.com/en/blog/cara-setting-nginx-reverse-proxy-docker-cloudflare/)

## Sources

- [RFC 1034: Domain names, concepts and facilities](https://www.rfc-editor.org/rfc/rfc1034)
- [RFC 1035: Domain names, implementation and specification](https://www.rfc-editor.org/rfc/rfc1035)
- [RFC 1912: Common DNS operational and configuration errors](https://www.rfc-editor.org/rfc/rfc1912)
- [RFC 9499: DNS terminology](https://www.rfc-editor.org/rfc/rfc9499)
- [RFC 3596: DNS extensions to support IPv6 (AAAA)](https://www.rfc-editor.org/rfc/rfc3596)
- [RFC 8659: DNS Certification Authority Authorization (CAA)](https://www.rfc-editor.org/rfc/rfc8659)
- [RFC 7208: Sender Policy Framework (SPF in TXT records)](https://www.rfc-editor.org/rfc/rfc7208)
- [Cloudflare docs: Proxy status (proxied and DNS only)](https://developers.cloudflare.com/dns/proxy-status/)
- [Cloudflare docs: Network ports the proxy supports](https://developers.cloudflare.com/fundamentals/reference/network-ports/)
- [Cloudflare docs: Set up CNAME flattening](https://developers.cloudflare.com/dns/cname-flattening/set-up-cname-flattening/)
- [Cloudflare docs: Time to live (TTL)](https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/)
- [Cloudflare docs: Web Analytics FAQ (beacon hosts and CSP)](https://developers.cloudflare.com/web-analytics/faq/)
- [AWS docs: Choosing between alias and non-alias records](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-choosing-alias-non-alias.html)
- [BIND 9 manual pages: dig](https://bind9.readthedocs.io/en/latest/manpages.html)

---

- HTML version: https://www.matthewswong.com/en/learn/devops-from-zero/dns-records-explained/
- Bahasa Indonesia: https://www.matthewswong.com/id/learn/devops-from-zero/dns-records-explained/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
