DEV Community

mattleeee
mattleeee

Posted on Originally published at hkcode.dpdns.org

The CNAME Trailing Dot: One Character That Broke the Domain

The CNAME Trailing Dot: One Character That Broke the Domain

The migration was supposed to take ten minutes. New domain registered on a free-tier registrar, GitHub Pages already serving the site on mattlee.github.io, so all that remained was a CNAME record pointing www.example.dev at the GitHub Pages hostname. I typed mattlee.github.io into the panel's value field, saved, and waited. Then I waited some more.

dig returned NXDOMAIN for the apex and SERVFAIL for www. GitHub Pages reported no custom domain configured. The DNS panel insisted the record existed, and it did — just not the record anyone intended to create.

The fix was a single character. The value needed to be mattlee.github.io., with the trailing dot. Without it, the registrar stored a relative name, and the record resolved to mattlee.github.io.example.dev. — a hostname inside my own zone that points at nothing.

What the trailing dot actually means

DNS has two kinds of names: absolute (fully qualified) and relative. The distinction is encoded in the presentation format by a terminal dot. mattlee.github.io. is an absolute name — it is fully specified and the resolver stops there. mattlee.github.io is a relative name — it is a prefix to be completed by appending an origin.

This is not a quirk of one vendor. RFC 1034 and RFC 1035 define the master file format with this convention: a domain name that does not end in a dot is interpreted relative to the current origin. In a zone file, the origin is the zone itself. So inside the zone for example.dev, the relative name mattlee.github.io expands to mattlee.github.io.example.dev.

A zone file makes this visible:

$ORIGIN example.dev.
www     IN  CNAME  mattlee.github.io.      ; absolute: mattlee.github.io.
blog    IN  CNAME  mattlee.github.io       ; relative: mattlee.github.io.example.dev.
Enter fullscreen mode Exit fullscreen mode

Both lines look plausible in a web form. Only the first one does what the author meant. The second creates a CNAME that points back into the same zone, to a name that has no A record. Resolvers chase it, find nothing, and return NXDOMAIN or SERVFAIL depending on how far the chain gets.

Why does the protocol work this way at all? Because relative names are useful. Zone files are full of them — www, mail, api — and appending the origin keeps configuration short and portable across zones. The trailing dot is the escape hatch that says "I am done, this is complete." The format is terse by design, and the dot is the only signal that distinguishes the two cases.

Why some panels add the dot and some don't

Registrar and DNS-provider UIs sit on a spectrum between two philosophies.

Zone-file faithful. Some panels treat the value field exactly as a zone file would. If you omit the dot, you get a relative name. These panels are technically correct and operationally hostile — most users typing a CNAME value mean an absolute hostname, because they copied it from a documentation page or a hosting dashboard where the trailing dot is usually omitted.

Lenient normalizers. Other panels assume every value is absolute and append the dot for you on save. Many also strip a dot you type in, then re-add it, so the stored record is always fully qualified. These panels hide the problem entirely, which is why the same record that works on one provider fails on another.

Hybrid. A third class stores what you type but normalizes on display, so the panel shows mattlee.github.io whether or not the dot is present. You cannot tell from the UI what was actually written to the zone. This is the worst case for debugging, because the panel's appearance actively misleads you.

The practical consequence: a CNAME value that works on your old registrar may break on your new one, even though both panels display the same string. The record is not the string you see. The record is what the provider serialized into the zone.

How to verify what was actually stored

Never trust the panel. Query the authoritative nameservers directly and read the ANSWER section.

dig www.example.dev CNAME +short
Enter fullscreen mode Exit fullscreen mode

If the record is correct, you get:

mattlee.github.io.
Enter fullscreen mode Exit fullscreen mode

Note the trailing dot in the output. dig always prints absolute names with the dot, because that is what the wire format contains — DNS messages carry fully qualified names, and the presentation layer adds the dot back on output.

If the record is broken, you get something like:

mattlee.github.io.example.dev.
Enter fullscreen mode Exit fullscreen mode

That is the smoking gun. The resolver is telling you the stored name is relative and the origin was appended. If you see the zone name glued onto the end of your intended target, the value field is missing its dot.

You can also query the authoritative server directly, bypassing caching and any recursive resolver behavior:

dig @ns1.registrar.net www.example.dev CNAME +noall +answer
Enter fullscreen mode Exit fullscreen mode

The +noall +answer flags trim the output to just the answer section, which is where the CNAME target lives. If the answer section is empty and the authority section shows NXDOMAIN, the name may not exist at all — check that the record saved and that the zone is active.

nslookup works too, though its output is noisier:

nslookup -type=CNAME www.example.dev ns1.registrar.net
Enter fullscreen mode Exit fullscreen mode

Look at the canonical name = line. Same rule: a trailing dot means absolute, no trailing dot in a zone-file-style answer means relative.

For a full trace of the resolution chain, including where it fails:

dig www.example.dev +trace
Enter fullscreen mode Exit fullscreen mode

+trace walks from the root down. If the delegation to your zone succeeds but the CNAME target never resolves, the failure is inside your zone — almost always the missing dot.

Automating the check

Once you know what to look for, the check is trivial to script. This is worth wiring into your deploy pipeline, because a broken CNAME is invisible until someone tries to load the site.

import subprocess
import sys


def get_cname(name: str, server: str | None = None) -> str | None:
    """Return the CNAME target for `name`, or None if no CNAME exists."""
    cmd = ["dig", "+short", "CNAME", name]
    if server:
        cmd.insert(1, f"@{server}")
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
    target = result.stdout.strip()
    return target or None


def check_cname(name: str, expected: str, server: str | None = None) -> bool:
    """
    Verify that `name` is a CNAME to `expected`.

    `expected` should be given without a trailing dot; the comparison
    normalizes both sides so the check is independent of presentation.
    """
    target = get_cname(name, server)
    if target is None:
        print(f"FAIL: no CNAME record for {name}")
        return False

    normalized = target.rstrip(".")
    if normalized != expected.rstrip("."):
        print(f"FAIL: {name} -> {target} (expected {expected}.)")
        print("      If the target ends in your zone name, the value is")
        print("      relative and is missing its trailing dot.")
        return False

    print(f"OK: {name} -> {target}")
    return True


if __name__ == "__main__":
    ok = check_cname("www.example.dev", "mattlee.github.io")
    sys.exit(0 if ok else 1)
Enter fullscreen mode Exit fullscreen mode

The rstrip(".") normalization matters: dig +short prints the absolute form with a dot, but you will often pass the expected value without one, as copied from a dashboard. Comparing the normalized forms avoids false alarms while still catching the real failure — a target that has your zone name appended.

If you want to detect the relative-name bug specifically, check whether the target ends with the zone:

def looks_relative(target: str, zone: str) -> bool:
    """Heuristic: a CNAME target that ends in the zone is probably relative."""
    return target.rstrip(".").endswith(zone.rstrip("."))
Enter fullscreen mode Exit fullscreen mode

This is a heuristic, not a proof — a legitimate CNAME can point into your own zone, and internal service records often do. But for a public web CNAME pointing at an external host, a target that ends in your own zone name is a near-certain sign of a missing dot.

The general lesson

DNS tools are literal and unapologetic. There is no fuzzy matching, no "did you mean," no warning when you save a relative name where you meant an absolute one. The zone is a text format with a precise grammar, and the provider either follows it or silently normalizes it for you. When it follows it, you get exactly what you typed, including the consequences.

This bites in predictable places:

  • CNAME and MX values are the most common victims, because they are the record types where you type a hostname rather than an IP address.
  • Copy-paste from documentation usually drops the trailing dot, because docs are written for humans and the dot is noise to most readers.
  • Provider migration surfaces latent bugs, because the new panel may interpret the value differently from the old one.
  • TXT records are not affected the same way — the content is an opaque string, not a domain name — but SPF and DKIM records that embed domain names inside the string can carry the same confusion.

The habit that prevents all of it: when a record value is a hostname, type the trailing dot. It is harmless on panels that normalize, and load-bearing on panels that don't. Then verify with dig and read the ANSWER section, because the panel is a UI and the zone is the truth.

The domain came up the moment the dot was added. The record had been "correct" in the panel for an hour. It had never been correct in the zone.

More notes like this ship every week on this site.


Daily Picks

The following pairs are selected from the multi-timeframe trend scanner (Gate.io futures) and are for technical-analysis study only — not investment advice.
Data updated: 2026-10-22 08:46:44

Short

Pair Signal Price Take Profit Stop Loss R/R
ARIA 短平快做空 $0.0655 $0.0635 $0.0671 1:1.2

1 picks selected. Scanner runs every 15 minutes.

Top comments (0)