Every checker passed a hostname that does not exist
The schema checked the shape. Nothing checked the name.
Here’s a category of mistake I didn’t have a name for until this morning: well-formed and wrong.
Not malformed. Malformed is easy — malformed is what every validator you own is already looking for, and it gets caught in the editor before you’ve finished typing it. Well-formed and wrong sails through all of it, because there is nothing about it to catch.
This is what one looks like.
- value: "jwogrady.netlify.ap."
+ value: "jwogrady.netlify.app."
One letter. That’s the whole bug.
What that line was supposed to do
jwogrady.com runs on my own nameservers — the machines that answer where
do I go? when somebody types the address. Their answers live in a zone file,
a plain text record of every name under the domain and where each points. The
site itself is hosted at Netlify, on a machine that answers to
jwogrady.netlify.app.
So the zone says: when somebody asks for www.jwogrady.com, send them to
jwogrady.netlify.app. That’s a CNAME — one name declaring itself an alias
for another.
The value above is that target, typed by hand, because it had to come from somewhere and the somewhere was a person reading it off another screen.
.ap is not a top-level domain. Nothing resolves there. Anybody arriving at
www.jwogrady.com would have been handed a pointer to nowhere.
The part I’d have bet money on
That repository doesn’t ship a zone without checking it. There’s a schema
validator, and there’s kzonecheck — the zone checker that ships with Knot
DNS, the nameserver software itself. Neither is decorative. Both have caught
real mistakes.
The typo was checked out into a worktree and both were run against it. The schema check:
$ ./bin/check-zones
zones: 65 nameservers: 3 primary: ns1
intent: parked=60 served=2 staged=3
schema clean
exit=0
Then kzonecheck, run on the nameserver itself, same binary version the live
nodes serve with, against the rendered zone:
kzonecheck version: kzonecheck, Knot DNS 3.5.8
--- kzonecheck on the TYPO zone ---
exit=0
Silent. Not a warning, not a note. No output at all.
Both would have passed jwogrady.netlify.banana. too, and both would have
passed a target pointing at a competitor’s website.
I want to be precise about why, because “the tools failed” is the lazy read
and it isn’t what happened. The tools did exactly what they say they do.
The schema asks: is this a well-formed record of this type — is there a value
where a value goes, is it shaped like a domain name, does it end in the dot a
fully qualified name should end in? Yes, yes, yes. kzonecheck asks: is this
file syntactically a zone, and is it internally consistent? It is.
Neither can ask the only question that mattered, and neither claims to:
Is there anything there?
That can’t be answered by reading the file. It can only be answered by leaving the machine and asking the internet — a different kind of operation, one that needs the network, can be slow, and can be down. Every validator you own works on text that is sitting still.
The schema checked the shape. Nothing checked the name.
The checkers are not toothless, and that’s the interesting part
Here’s where I’d have written something unfair if I’d stopped there.
The same session tried a second, deliberately broken zone — this one with a CNAME at the apex, which DNS flatly forbids. A zone’s apex has to carry its SOA and NS records by definition, and a CNAME is not allowed to coexist with other data at the same name. RFC 1034 §3.6.2, tightened by RFC 2181 §10.1.
The schema check passed that too:
$ ./bin/check-zones
schema clean
exit=0
kzonecheck did not:
[jwogrady.com.] another record exists beside CNAME
Error summary:
1 another record exists beside CNAME
error: serious semantic error detected
exit=1
Six words that are the entire rule. So the stack works. On a structural
error the layers do their job — schema misses it, kzonecheck catches it,
and it never reaches a node.
That contrast is the honest shape of this. The validation isn’t weak and it isn’t theatre. There is simply one class of error it structurally cannot see, because nothing anywhere in it resolves a name. You can add checkers all day and never close that gap, because the gap isn’t a missing rule. It’s a missing phone call.
How it got caught
Not by a tool. Somebody read the value, thought .ap isn’t a top-level
domain, and went and asked:
jwogrady.netlify.app -> ['98.84.224.111', '18.208.88.157']
jwogrady.netlify.ap -> FAIL: [Errno -2] Name or service not known
The fix commit then put the finding where the mistake happens — at the top of the zone file, above the records, so the next person to hand-type a target reads this before they type:
bin/check-zonespassed on the typo and would pass on any other well-formed name, because the schema validates a record’s shape and kzonecheck validates syntax. Neither resolves anything. This is the gap, so the header now says so and shows the dig that closes it.
That’s the only reliable way I know to stop a mistake repeating. Not remembering harder. Leaving the note where the next person will be standing.
And resolving it still wouldn’t have been enough
This is the part that got me sitting up, and it’s why this is an entry rather than a footnote.
“Resolve it, don’t read it” catches .ap, because .ap isn’t a top-level
domain and nothing on earth answers there. Now suppose the slip had been
jwogrday.netlify.app. — same letters, two of them swapped, correct .app
ending.
*.netlify.app is a wildcard. Every name under it resolves, whether or
not anyone has ever deployed a thing there. That transposition would have
resolved perfectly, handed back real addresses, and been just as wrong.
The check goes green on a pointer to nothing of mine.
So the session didn’t stop at resolving. It fetched what was there and made
it prove its identity: HTTP 200, <title>jwogrady</title>, and a generator
naming Hugo 0.166.0 — the exact version pinned as HUGO_VERSION in this
site’s own netlify.toml.
That’s the step that actually closes it. Not does something answer, but is the thing that answered mine — tested against a fact I already hold on my own side of the wire.
The bit that actually spooked me
Re-read the line. Go on.
jwogrady.netlify.ap.
My eye still wants to tell me it’s fine. If you have ever proofread your own writing and sailed past the same typo four times running — yes. That. Your eye is not a checking mechanism, it’s a pattern-completer, and it completes this one every time because it knows what the word is meant to be.
So the value doesn’t look wrong to the machine and it doesn’t look wrong to you. Those are supposed to be two independent checks. They aren’t. They fail together, on the same input, for related reasons.
This is not a DNS problem
I keep wanting to write this up as a DNS lesson and it isn’t one. DNS is just where it bit me today.
Anywhere a human types an identifier that a machine will accept, you have this. The validator checks the format. The format was never the risk.
- A bank transfer to an account number that passes its checksum and belongs to somebody else. The checksum is a shape check. It is not a name check.
- An invoice emailed to a well-formed address at a company that no longer uses that address. Your mail client will not tell you. It has no way to know.
- A tracking tag on a landing page firing into a property you stopped using in March. It’s live. It’s reporting. It’s reporting to nowhere you look.
Every one of those passes the only gate most operations have, because the gate inspects form. And every one of them is found the same way: not by looking harder at what you wrote, but by asking the far end whether it’s there.
What to carry out
Don’t proofread the pointer. Make the far end prove it’s yours.
Three rungs, and most of us stop on the first:
- Does it look right? — your eye. Worth nothing, as established.
- Does something answer? — better, and still not enough anywhere wildcards, catch-alls and default routes exist. Which is everywhere.
- Is the thing that answered mine? — what came back, checked against something you already knew independently.
Pick the thing in your business that got typed once and has quietly pointed somewhere ever since. The forwarding address. The account number on the recurring payment. The webhook URL. The emergency contact on the alarm system. Don’t read it.
Send the pound and check whose account it landed in. Email the address and make somebody reply. Load the URL and confirm it’s your own page looking back at you. Ring the number and find out who picks up.
Something answering is not the same as the right thing answering, and the gap between those two is where a year of quiet wrongness lives.