Module 02 — Resource Records, Zone Files & Reading a Domain

Updated 20 August 2026

Module 02 · Resource Records, Zone Files & Reading a Domain

Module 01 gave you the shape of a record and exactly one type. This module gives you the rest of the types, the file they are written in, and — in Part D — the working recipes for the job you are actually handed: "here is a domain, tell me what exists under it and where it points".

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Module 01. You already know how to read a dig response, what the aa flag means, how TTLs count down, and how to tell NXDOMAIN from NODATA — all four are used constantly here.


Part A · The anatomy of a record

A1 · The five fields, and why DNS thinks in sets

The analogy. Think of a box of six eggs. You do not buy one egg out of the box — you buy the box. Every egg in it has the same date on the label, and when you replace it you replace the whole box.

DNS works the same way. That is why all the records of one type share a TTL, and why an automation script that "adds one record" through an API can silently throw the other five away.

Every record, of every type, has the same five fields. You met them in Module 01:

plain text
seamless.se.        60      IN      A       52.77.52.233
│                   │       │       │       └── RDATA - the value, format depends on TYPE
│                   │       │       └────────── TYPE  - what kind of record
│                   │       └──────────────── CLASS - always IN in practice
│                   └─────────────────────── TTL   - seconds this may be cached
└──────────────────────────────────── NAME  - the owner name

What Module 01 did not tell you is that DNS almost never handles one record at a time.

The RRset is the real unit of DNS. An RRset is every record sharing the same owner name, class and type. When seamless.se has six MX records, those six are one RRset. DNS moves, caches, replaces and signs whole RRsets — never individual records inside one.

Three consequences follow directly from that, and they explain behaviour that otherwise looks arbitrary:

  1. Every record in an RRset must have the same TTL. They expire together because they are cached as one object. If you configure different TTLs, the server picks one and silently discards the others
  2. You cannot add or delete "one record". You replace the whole set. This is why provider consoles make you edit a multi-line text box rather than a single value, and why a careless API call that sends one A record wipes out the other five
  3. There is no order. The set is unordered; any rotation you see is the server or resolver shuffling on the way out
This is the single most expensive misunderstanding in DNS automation. A script that "adds an A record" through an API which takes the full RRset, but only sends the new value, has just deleted every other address for that name. It will look like it worked. It returns success. The outage arrives when the traffic that used to reach the other five servers stops.
🧪 Exercise A1.1 — Find an RRset with more than one member and check the TTLs
bash
dig +noall +answer seamless.se MX
Expected result — click to reveal
plain text
seamless.se.		3600	IN	MX	10 aspmx.l.google.com.
seamless.se.		3600	IN	MX	20 alt1.aspmx.l.google.com.
seamless.se.		3600	IN	MX	20 alt2.aspmx.l.google.com.
seamless.se.		3600	IN	MX	30 aspmx2.googlemail.com.
seamless.se.		3600	IN	MX	30 aspmx3.googlemail.com.

The order you see will differ — the set is unordered.

Five records, one RRset. Same owner name, same class, same type. And look at the TTL column: 3600 on every single line, not by coincidence but because it is structurally impossible for them to differ.

Read one more thing out of this. The numbers 10, 20, 30 are inside the RDATA — they are part of the MX record's value, not a DNS-level priority. DNS itself has no concept of preference or ordering; the mail client parses those numbers. Every piece of "intelligence" you see in DNS lives in the RDATA and is interpreted by the client, never by DNS. Hold on to that, because it explains why DNS load balancing is as limited as it is.

Now imagine this at 500 hosts. An engineer adds a sixth mail server via the API by sending a single MX record. The API replaces the RRset. Five mail servers vanish from DNS in one call, and nothing errors.

🎯 Interview questions — Records and RRsets

Q. What is an RRset?

All records sharing the same owner name, class and type. DNS treats it as a single indivisible object — it is cached, transferred, replaced and DNSSEC-signed as a unit.

The practical rule that follows is that every record in an RRset must carry the same TTL, because they are cached as one object and expire together.

The detail that separates a strong candidate: knowing that this is why most DNS APIs are replace-the-whole-set rather than add-one-record, and why a naive automation script that sends a single value silently deletes the rest. It also explains DNSSEC: signatures cover RRsets, not individual records, which is why you cannot pick one address out of a signed set without breaking the signature.

Q. Can two records with the same name and type have different TTLs?

No. RFC 2181 requires the TTLs within an RRset to be identical. If a zone file specifies different values the server will choose one — typically the first or the lowest, depending on implementation — and discard the rest, usually with a warning you will never see because nobody reads zone-load logs.

Different types at the same name are different RRsets and may absolutely have different TTLs, which is why seamless.se can serve A at 60 seconds and MX at 3600 seconds simultaneously.


A2 · Asking for a type, and why ANY is a dead end

The analogy. Think of walking into a shop and saying "show me everything you have about me". No shop does that. You have to ask for each thing separately: my order, my address on file, my receipt.

DNS is the same. There is no "show me everything" button — you ask for one type at a time, in a loop. ANY sounds like the button and never was.

In Module 01 every query was type A, because that is dig's default. You choose a different type by naming it:

bash
dig seamless.se MX          # type after the name
dig -t MX seamless.se       # or with -t, identical
dig MX seamless.se          # dig is relaxed about ordering

Every beginner then asks the obvious question: is there a way to get everything at once? There is a query type called ANY, and it does not do that.

ANY never meant "give me all records". It means "give me whatever you happen to have in cache for this name right now" — so against a resolver it returns an arbitrary subset, and against an authoritative server it was never a reliable inventory either.

Worse, it became the preferred tool of DNS amplification attacks, because a tiny query produced an enormous response. So in 2019 the major operators acted: Cloudflare returns a minimal synthetic answer, and RFC 8482 formally blessed refusing to give a conventional ANY response.

The operational conclusion: if you want to know every record type at a name, you must ask for each type in turn. There is no listing operation in the DNS protocol. This is not a limitation of your tools — it is a property of the protocol, and Part D is built entirely around the consequences.

🧪 Exercise A2.1 — Try ANY, watch it disappoint you, then do it properly
bash
dig seamless.se ANY +noall +answer

echo "--- the honest way ---"
for t in SOA NS A AAAA MX TXT CAA; do dig +noall +answer seamless.se $t; done
Expected result — click to reveal
plain text
$ dig seamless.se ANY +noall +answer
seamless.se.		3600	IN	HINFO	"RFC8482" ""

--- the honest way ---
seamless.se.		900	IN	SOA	ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400
seamless.se.		3600	IN	NS	ns-135.awsdns-16.com.
seamless.se.		3600	IN	NS	ns-1490.awsdns-58.org.
seamless.se.		3600	IN	NS	ns-1760.awsdns-28.co.uk.
seamless.se.		3600	IN	NS	ns-722.awsdns-26.net.
seamless.se.		60	IN	A	52.77.52.233
seamless.se.		3600	IN	MX	10 aspmx.l.google.com.
seamless.se.		3600	IN	MX	20 alt1.aspmx.l.google.com.
seamless.se.		3600	IN	MX	20 alt2.aspmx.l.google.com.
seamless.se.		3600	IN	MX	30 aspmx2.googlemail.com.
seamless.se.		3600	IN	MX	30 aspmx3.googlemail.com.
seamless.se.		300	IN	TXT	"v=spf1 include:amazonses.com include:_spf.linserv.se include:spf.mandrillapp.com include:_spf.salesforce.com a:mail.workbuster.se include:_spf.google.com ip4:3.1.214.242 ip4:14.142.43.230 ip4:14.99.30.2 ~all"

ANY gave you a joke record. HINFO "RFC8482" is a deliberate placeholder meaning "I am not answering ANY queries; ask for a specific type". Depending on which resolver you hit you may instead get a random handful of records, or a REFUSED. None of those is an inventory.

The loop is the real answer, and notice what it revealed that a single query never could:

  • Four NS records, all awsdns — this zone is hosted on AWS Route 53. That one line tells you where to go to make changes, and it tells you Route 53's quirks apply
  • AAAA and CAA produced no output at all. Not an error — NODATA. This domain publishes no IPv6 address and no certificate-issuance policy. Both are findings
  • The A record TTL is 60 while everything else is 3600. Somebody deliberately made the address agile. That is the signature of a host that gets repointed — a failover target, or a migration in progress
  • The SPF record in TXT lists ip4: addresses. Those are mail-sending hosts, and they are a free hint about the organisation's own IP ranges. Part D uses this

Now imagine this at 500 domains. "Ask for each type in turn" is a loop, not a task — which is exactly why the audit script in D5 exists.

🎯 Interview questions — Query types

Q. How do you list all the records for a domain?

You cannot — and recognising that the question contains a false premise is the answer they are looking for. DNS has no listing operation. It answers questions of the form "what is at this exact name, of this exact type", and nothing else.

The practical approaches are: query each type you care about in turn; attempt a zone transfer if you are permitted one; or, most reliably, read the zone from the provider's API or console, because the provider does hold the full list.

The trap in this question is ANY. Candidates reach for it and it has never done what they think. It returns cached fragments, and since RFC 8482 most large operators return a minimal synthetic response instead — partly because ANY was the engine of DNS amplification attacks.


Part B · The record types you will actually meet

B1 · A and AAAA — addresses

The analogy. Think of a country that ran out of postcodes and introduced a new longer format. Both formats work. Some buildings have both, some only the old one, and some new residents can only use the new one.

If you only ever test with the old format, you will swear everything is fine while a growing group of people cannot reach you at all.

TypeRDATANotes
AOne IPv4 addressFour bytes on the wire. AAAA is not "A for IPv6" in format, only in purpose
AAAAOne IPv6 addressSixteen bytes — four times four, which is where the four As come from
One record holds exactly one address. Multiple addresses means multiple records in one RRset — and, per A1, they share a TTL and have no order. A client receiving six addresses picks one by its own rules, which is why "DNS round robin" distributes lookups rather than load, and why one dead server out of six produces intermittent failures rather than a clean outage.
A and AAAA are separate RRsets and they can disagree. A name can have an A pointing at a working server and an AAAA pointing at one that was decommissioned. Clients on IPv6-capable networks prefer the AAAA — so the site is broken for them and fine for everyone else, and every test you run from your IPv4 laptop passes. When a fault affects "some users, seemingly at random", query both types before anything else.
🧪 Exercise B1.1 — Ask both address types, and notice the absence
bash
dig +noall +answer seamless.se A
dig +noall +comments seamless.se AAAA
Expected result — click to reveal
plain text
$ dig +noall +answer seamless.se A
seamless.se.		60	IN	A	52.77.52.233

$ dig +noall +comments seamless.se AAAA
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44118
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

The second one is the exercise. status: NOERROR with ANSWER: 0 — the NODATA case from Module 01 D2, met in the wild. The name exists, it has records, it has no IPv6 address.

Why this is a finding and not a curiosity. An IPv6-only client cannot reach this site at all without a translation layer, and a growing number of mobile networks are IPv6-only. "No AAAA" is a deliberate choice or an oversight, and the audit tells you which by whether anything else in the zone has one.

And note the TTL of 60 on the A record while the zone's other records sit at 3600. Somebody wanted to be able to move this address quickly. That is worth asking about — it usually means a failover mechanism or an unfinished migration.


B2 · CNAME — the alias, and the rule that makes it dangerous

The analogy. Think of filing a mail-redirection order at the post office. Once you say "forward everything from this address", everything goes — letters, parcels, bank statements, the lot. You cannot say "forward my letters but keep delivering parcels here".

That is the whole CNAME rule in one sentence. It redirects the address, not one kind of post. So a name with a CNAME can hold nothing else — and filing one on your main address is how a company's email quietly stops arriving.

A CNAME says: this name is an alias; the real name is that one. The resolver then starts again at the target.

The mechanism, and every CNAME rule falls out of it. A CNAME does not redirect one record type — it redirects the name itself. Once a name is an alias, every lookup of every type at that name is answered by following the alias. The name no longer has an identity of its own.

So: a name with a CNAME cannot have any other record. Not an MX, not a TXT, not an A. There is nowhere to put them, because the name is not a destination any more — it is a signpost. You do not need to memorise that rule if you understand the sentence above; you can derive it.

The consequence that ends careers quietly. You own example.com with working MX records. Someone asks you to point the bare domain at a CDN and you add a CNAME at example.com itself. If the server accepts it, your mail stops — the MX records are now unreachable, because the name is an alias and mail lookups follow it to the CDN, which does not accept your mail.

This is why a CNAME at the zone apex is forbidden: the apex always carries SOA and NS records, so it can never be an alias. C3 covers what your provider offers instead.

🧪 Exercise B2.1 — Follow a real alias chain, then watch a CNAME swallow another type
bash
# www is an alias. Watch dig return BOTH the alias and the resolved target.
dig +noall +answer www.seamless.se

# now ask that same alias for a completely different type
dig +noall +comments +answer www.seamless.se MX
Expected result — click to reveal
plain text
$ dig +noall +answer www.seamless.se
www.seamless.se.	3600	IN	CNAME	seamless.se.
seamless.se.		60	IN	A	52.77.52.233

$ dig +noall +comments +answer www.seamless.se MX
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 30277
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

www.seamless.se.	3600	IN	CNAME	seamless.se.

First command — two records came back for one question. You asked for A at www.seamless.se and got a CNAME plus the A at the target. The resolver followed the alias for you and returned the whole chain as evidence. Note the two different TTLs: 3600 on the alias, 60 on the address. They are different RRsets at different names, cached and expiring independently — which means the alias can outlive the address it points at.

Second command is the important one. You asked for MX and got a CNAME back with ANSWER: 1. The server is telling you: this name is an alias, go and ask the target instead. There is no MX record here and there never can be.

That is the whole danger in one screen. If www.seamless.se were the domain receiving mail, its mail would follow this alias to wherever the target points. A CNAME does not politely apply to the record type you were thinking about — it captures the name completely.

A practical tell for audits: any +short output whose first line is a hostname rather than an IP address is a CNAME chain. Chains of three or four are common with CDNs and SaaS vendors, each hop adds latency on a cache miss, and each hop is a third party who can break you.

🎯 Interview questions — CNAME

Q. What is a CNAME and what are its restrictions?

An alias: it declares that the canonical name of this name is some other name, and the resolver restarts its lookup there.

The restriction is that a name owning a CNAME can own no other record type, because the CNAME redirects the whole name rather than one record type. That in turn makes a CNAME illegal at the zone apex, since the apex must carry SOA and NS.

What I would add to show I understand the mechanism rather than the rule: the danger is silent. Adding a CNAME to a name that already has MX or TXT records does not usually produce an error — it produces mail that stops being delivered and domain-verification records that stop being found. And CNAMEs cost you a second resolution step, so long vendor chains add real latency on every cache miss.

Q. Why can't you put a CNAME at the apex of a zone, and what do you do instead?

Because the apex is required to carry SOA and NS records, and a CNAME excludes all other types at the same name. It is a structural contradiction, not a policy choice.

In practice you use a provider-specific pseudo-record: Route 53 alias records, or the ALIAS / ANAME / CNAME flattening offered by other providers. These are not DNS features — the authoritative server resolves the target internally and answers with a real A or AAAA, so what goes on the wire is a plain address record and standards compliance is preserved.

The operational catch worth naming: because the provider resolves the target for you, the answer is only as fresh as the provider's own resolution, and the flattening is done from the provider's network location. For a geo-distributed target that can hand your users a suboptimal address, and it is invisible unless you go looking.


B3 · NS — where authority is handed over

The analogy. Think of head office saying "we don't keep your file here — the Penang branch does". Head office does not know your account balance and never did. It knows which branch to send you to.

And the name on the branch's sign tells you something for free: which company actually runs it. That is why one NS lookup tells you the DNS provider, and therefore which console to log into.

An NS record names a server that is authoritative for a zone. In practice it is the record you look up first on any unfamiliar domain, because it answers two questions at once: who do I ask for a guaranteed-fresh answer, and who actually operates this domain's DNS.

Why the second question is answered too. Nobody writes their own nameserver hostnames any more. The NS records almost always carry the provider's branding — awsdns for Route 53, nsone for NS1, cloudflare.com, azure-dns, googledomains. So one NS lookup tells you which console to log into, which API to script against, and which set of provider-specific quirks apply to this zone. On an unfamiliar estate that is often the single most useful lookup you can run.
🧪 Exercise B3.1 — Find the authoritative servers, then use one
bash
dig +noall +answer seamless.se NS

# now ask one of them directly, and watch the header change
dig @ns-135.awsdns-16.com seamless.se A +noall +comments +answer
Expected result — click to reveal
plain text
$ dig +noall +answer seamless.se NS
seamless.se.		3600	IN	NS	ns-135.awsdns-16.com.
seamless.se.		3600	IN	NS	ns-722.awsdns-26.net.
seamless.se.		3600	IN	NS	ns-1490.awsdns-58.org.
seamless.se.		3600	IN	NS	ns-1760.awsdns-28.co.uk.

$ dig @ns-135.awsdns-16.com seamless.se A +noall +comments +answer
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35503
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

seamless.se.		60	IN	A	52.77.52.233

Four findings from eight lines of output.

1. awsdns means Route 53. Someone hands you this domain and you now know where the records live without asking anyone.

2. Four different top-level domains — .com, .net, .org, .co.uk. That is deliberate, and it is good practice worth recognising. If one TLD's infrastructure has a bad day, three of your four nameservers are still reachable. Providers who put all four nameservers under one domain have quietly given you a shared failure mode.

3. The second query has aa set and no ra. Exactly as predicted in Module 01 B1: an authoritative server marks its answer authoritative and does not offer recursion. This is your ground truth — when you need to know what the zone actually says rather than what a cache remembers, this is the query.

4. The TTL is 60 here too, and it is the full published value rather than a counted-down one, which confirms it: the zone really does publish a 60-second address TTL.

The habit to build. Whenever you are verifying a change, or you suspect a cache, run the NS lookup and then re-run your query against one of the returned servers. Two commands, and you have removed every cache in the world from the investigation.

🎯 Interview questions — NS records

Q. What does an NS record do?

It names an authoritative server for a zone. NS records appear in two places: inside the zone itself at the apex, and — more importantly — in the parent zone, where they form the delegation.

Operationally it is the first lookup I run on an unfamiliar domain, because the nameserver hostnames identify the DNS provider, which tells me where to make changes and which provider-specific behaviours to expect.

The detail that matters and catches people out: the authoritative set is the one published by the parent, not the one inside the zone. The two are supposed to agree, and when they do not you get a delegation inconsistency that produces intermittent, resolver-dependent failures — the kind where the problem follows some users and not others. Module 03 covers how a resolver actually uses these.


B4 · SOA — the zone's control panel

The analogy. Think of the cover sheet on a shared document. It says who owns it, who to email about it, what version this is, and how often other people should check for a newer copy.

The version number is the important bit. Everyone else decides whether to bother re-downloading by looking at that number — not by reading the document. Change the document and forget to change the number, and nobody ever gets your edit.

You have already met the SOA twice without being told what it was: it is the record that appeared in the AUTHORITY section of every negative answer in Module 01. Exactly one exists per zone, at the apex, and it holds seven fields.

plain text
seamless.se.  900  IN  SOA  ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400
                            │                      │                             │ │    │   │       │
                            │                      │                             │ │    │   │       └─ MINIMUM  86400
                            │                      │                             │ │    │   └───────── EXPIRE  1209600
                            │                      │                             │ │    └───────────── RETRY      900
                            │                      │                             │ └────────────────── REFRESH   7200
                            │                      │                             └──────────────────── SERIAL      1
                            │                      └────────────────────────────────────────────────── RNAME
                            └───────────────────────────────────────────────────────────────────────── MNAME
FieldWhat it is for
MNAMEThe primary nameserver — where changes are made. Secondaries send dynamic updates here
RNAMEThe responsible party's email address, written as a domain name. The first dot is the @, so awsdns-hostmaster.amazon.com. means [email protected]
SERIALA version number. A secondary transfers the zone only when the primary's serial is higher than its own
REFRESHHow often a secondary checks the primary's serial, absent a NOTIFY
RETRYHow soon to retry after a failed check
EXPIREHow long a secondary keeps serving the zone while it cannot reach the primary. After this it stops answering entirely — which is safer than serving data of unknown age
MINIMUMOriginally a default TTL. Since RFC 2308 it means the negative caching TTL — how long a "this does not exist" may be cached
This is the answer to the mystery from Module 01 D2. You create a record and it keeps returning NXDOMAIN. The reason is right here: the absence was cached, and its lifetime comes from the SOA, not from the shiny 60-second TTL you set on the new record.

And there is a precision here that most write-ups get wrong. RFC 2308 says the negative TTL is the lesser of the SOA's MINIMUM field and the SOA record's own TTL. For seamless.se that is min(86400, 900) = 900 seconds, not 86400. Knowing which of the two numbers actually governs is the difference between predicting a 15-minute wait and a 24-hour one.

That serial number is 1, and it never changes. On a textbook BIND zone you would expect 2026081701 — the YYYYMMDDnn convention — and incrementing it is mandatory or your secondaries never pick up the change.

Route 53 does not work that way: it replicates internally rather than by AXFR, so the serial is decorative. The lesson is not "serials do not matter" — it is "serials matter exactly as much as your replication mechanism depends on them". On BIND primary/secondary, forgetting to bump the serial is the classic "I changed it and only some servers agree" outage. Module 05 makes you commit it deliberately.

🧪 Exercise B4.1 — Read a real SOA and compute the negative cache time yourself
bash
dig +noall +answer seamless.se SOA

# now prove the negative TTL is real: ask for a name that does not exist,
# twice, and watch the SOA's TTL count down in the AUTHORITY section
dig no-such-name.seamless.se +noall +authority
sleep 10
dig no-such-name.seamless.se +noall +authority
Expected result — click to reveal
plain text
$ dig +noall +answer seamless.se SOA
seamless.se.		900	IN	SOA	ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

$ dig no-such-name.seamless.se +noall +authority
seamless.se.		900	IN	SOA	ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

$ sleep 10 ; dig no-such-name.seamless.se +noall +authority
seamless.se.		890	IN	SOA	ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

You are watching a "no" expire. The TTL went 900 → 890 across a 10-second sleep, exactly like a positive answer would. The resolver has cached the non-existence of no-such-name.seamless.se and is counting it down.

Do the arithmetic that matters. MINIMUM is 86400 — a full day. The SOA record's own TTL is 900. RFC 2308 takes the lesser, so a negative answer for this zone lives 15 minutes, not 24 hours. If you had read only the MINIMUM field you would have told your team to wait a day, and been wrong by a factor of 96.

The operational rule this gives you. After creating a new name, if you get NXDOMAIN, do not repeat the query against the same resolver and do not panic. Query an authoritative server — from B3 — which holds no negative cache. If it has the record, your change landed and you are simply waiting out min(MINIMUM, SOA TTL).

And the rule that prevents the problem entirely: never query a name before you publish it. The NXDOMAIN you cache is your own doing. This is why deployment scripts that "check if the record exists yet" in a tight loop make the wait longer on every resolver they touch.

🎯 Interview questions — SOA

Q. Walk me through the SOA record.

Seven fields: MNAME, the primary nameserver; RNAME, the responsible email address written as a domain name with the first dot standing in for the @; SERIAL, a version number; then REFRESH, RETRY and EXPIRE, which are the secondary's timers; and MINIMUM, which since RFC 2308 is the negative caching TTL.

EXPIRE is the one worth calling out: when a secondary has been unable to reach the primary for that long it stops answering for the zone entirely, rather than continuing to serve data of unknown age. That is a deliberate choice of a hard failure over a silent one.

Where a strong candidate goes further: the negative TTL is not simply the MINIMUM field — it is the lesser of MINIMUM and the SOA record's own TTL. That distinction decides whether a freshly created record is invisible for fifteen minutes or for a day, and almost every blog post gets it wrong.

Q. Why does the serial number matter, and what happens if you forget to increment it?

Secondaries compare the primary's serial with their own and transfer only when it is higher. If you edit a zone and do not bump the serial, the primary serves the new data and every secondary keeps serving the old — so answers depend on which server a resolver happened to ask.

That produces the worst class of DNS bug: intermittent, user-dependent, and not reproducible on demand.

The nuance worth adding: this only applies where replication is serial-driven, as in BIND primary/secondary. Managed providers such as Route 53 replicate internally and leave the serial at a constant — Route 53 publishes 1 and never changes it. So the right instinct is not "always bump the serial" but "know whether your replication depends on it", and the SOA tells you which world you are in.


B5 · MX — mail routing

The analogy. Think of a company whose shopfront and mail room are in different buildings. Customers walk into the shop; the post goes somewhere else entirely. Knowing where the shop is tells you nothing about where to deliver a letter.

The preference numbers are simply "try the main mail room first; if it is shut, use the back-up one; if that is shut too, try the third".

An MX record has two parts in its RDATA: a preference number and a hostname. Lower preference wins; equal preferences are load-shared.

Two rules that are violated constantly.

1. An MX target must be a hostname with an address record — never an IP address, and never a CNAME. MX 10 192.0.2.5 is invalid; so is an MX pointing at an alias. Many servers accept both and some senders will still deliver, which is worse than a clean rejection, because the failure is partial and blamed on the recipient.

2. The MX lives on the domain in the email address, not on the web server. Mail for [email protected] follows the MX of seamless.se — which is why a CNAME at that apex would destroy mail delivery, exactly as B2 warned.

🧪 Exercise B5.1 — Read a real mail configuration and follow it one hop further
bash
dig +noall +answer seamless.se MX
dig +short aspmx.l.google.com A

# a domain that publishes it accepts NO mail at all
dig +noall +answer example.com MX
Expected result — click to reveal
plain text
$ dig +noall +answer seamless.se MX
seamless.se.		3600	IN	MX	10 aspmx.l.google.com.
seamless.se.		3600	IN	MX	20 alt1.aspmx.l.google.com.
seamless.se.		3600	IN	MX	20 alt2.aspmx.l.google.com.
seamless.se.		3600	IN	MX	30 aspmx2.googlemail.com.
seamless.se.		3600	IN	MX	30 aspmx3.googlemail.com.

$ dig +short aspmx.l.google.com A
64.233.181.26

$ dig +noall +answer example.com MX
example.com.		86400	IN	MX	0 .

The first output is a complete mail architecture in five lines. aspmx.l.google.com is Google Workspace, so this organisation's mail is hosted by Google. Preference 10 is the primary pair-of-hands, 20 is the fallback, 30 is the fallback's fallback. A sender walks up that list.

Note the two records at preference 20. Equal preference means the sender picks between them freely — this is deliberate load sharing, and it is the only place in core DNS where a weighting-like behaviour exists at all.

The second command is the habit worth forming. An MX target is only useful if it resolves. Following it one hop confirms the chain is intact; an MX pointing at a hostname with no address record is a silent mail outage that no MX-level check would catch.

The third output is the interesting one. MX 0 . is the null MX from RFC 7505 — a single MX whose target is the root. It is not a mistake or an empty value; it is an explicit, machine-readable declaration: this domain accepts no mail, do not queue and retry for five days, reject immediately.

Why that matters in production. Most organisations own domains that never receive mail — parked brands, redirect domains, internal-only names. Without a null MX, a sender falls back to the domain's A record for delivery, spends days retrying against a web server, and generates support tickets. One record removes the whole class of problem. It is also a small anti-spoofing win: a domain that visibly accepts no mail is a less attractive forgery target.


B6 · TXT — SPF, DKIM, DMARC and proof of ownership

The analogy. Think of the noticeboard by a building's front door. Anyone walking past can read it, and people pin all sorts of things to it: which courier companies are allowed to deliver in your name, a code proving you really are the building owner, and a note telling the postman what to do with suspicious letters.

It is a general-purpose noticeboard, which is exactly why so many different systems ended up using it.

TXT holds free-form text, which is precisely why it became the dumping ground for every protocol that needed to attach data to a name. Four uses dominate:

UseWhere it livesWhat it declares
SPFThe domain itselfWhich servers may send mail as this domain
DKIM<selector>._domainkey.<domain>A public key used to verify message signatures
DMARC_dmarc.<domain>What a receiver should do when SPF and DKIM fail, and where to send reports
Ownership proofsThe domain, or _acme-challenge"I control this domain" — for Google, Microsoft, Atlassian, and for Let's Encrypt DNS-01
Why the underscore prefixes. _dmarc, _domainkey and _acme-challenge all begin with an underscore because, as Module 01 A4 established, an underscore is legal in a DNS name but illegal in a host name. That guarantees these control records can never collide with a real machine. It is a deliberate use of the gap between the two rule sets.
🧪 Exercise B6.1 — Read a real mail-security posture
bash
dig +short seamless.se TXT
dig +short _dmarc.seamless.se TXT
Expected result — click to reveal
plain text
$ dig +short seamless.se TXT
"v=spf1 include:amazonses.com include:_spf.linserv.se include:spf.mandrillapp.com include:_spf.salesforce.com a:mail.workbuster.se include:_spf.google.com ip4:3.1.214.242 ip4:14.142.43.230 ip4:14.99.30.2 ~all"

$ dig +short _dmarc.seamless.se TXT
"v=DMARC1; p=reject; pct=25; rua=mailto:[email protected]"

The SPF record is an org chart. Six include: mechanisms name six third parties permitted to send mail as this domain — Amazon SES, a Swedish hosting provider, Mandrill, Salesforce, a recruitment platform, and Google Workspace. Each one is a vendor relationship and each one is a party who can send mail as you.

~all is a soft fail, meaning "mail from anywhere else is suspicious but do not reject it outright". -all would be a hard fail. Soft fail is the cautious setting people adopt during rollout and then never tighten.

Now read the DMARC record together with it, because that is where the real posture is. p=reject is the strongest policy — but pct=25 means it is applied to only 25% of failing mail. That combination is the signature of a staged rollout in progress: the operator is ramping up and watching the rua= aggregate reports before going to 100%.

The finding to write down. p=reject; pct=25 reads as "strict" at a glance and is enforcing on a quarter of traffic. If nobody owns finishing the ramp, it sits there for years and everyone believes the domain is protected. Reading pct= is what separates an auditor from someone skimming for the word reject.

Two operational cautions on SPF. There is a hard limit of 10 DNS lookups when evaluating a record, and every include: costs at least one — six includes plus an a: is already close to the ceiling. Exceed it and the result is permerror, which many receivers treat as a failure: your legitimate mail starts bouncing with no change on your side, because a vendor expanded their record. Second, a single string in a TXT record cannot exceed 255 characters, so long records are split into several strings that are concatenated — which is why quoting matters when you write them.

🎯 Interview questions — TXT, SPF and DMARC

Q. How are SPF, DKIM and DMARC represented in DNS?

All three are TXT records. SPF sits on the domain itself and lists the hosts permitted to send as it. DKIM sits at <selector>._domainkey.<domain> and publishes a public key that receivers use to verify a signature in the message header. DMARC sits at _dmarc.<domain> and states what to do when the other two fail, plus where to send reports.

The underscore prefixes are deliberate: underscores are legal in DNS names but not in host names, so these can never collide with a real machine.

The operational detail that marks experience: SPF has a hard limit of ten DNS lookups during evaluation, and each include: consumes at least one. Since includes point at vendors' records, a vendor expanding theirs can push you over the limit and start bouncing your mail with no change on your side. Anyone who has run mail for a large organisation has been bitten by this, and mentioning it lands.

Q. A domain publishes p=reject in DMARC. Is its mail protected?

Not necessarily — you have to read the pct= tag alongside it. p=reject; pct=25 applies the reject policy to only a quarter of failing messages, which is a rollout in progress rather than a finished deployment.

I would also check the alignment mode and whether rua= reports are actually going somewhere a human reads.

The point I would make explicitly: p=reject is the tag people screenshot for the compliance evidence, and pct= is the tag that says what is really enforced. A staged rollout nobody ever finished looks identical to a completed one unless you read the second tag.


B7 · PTR — reverse DNS

The analogy. Think of the name plate screwed to a building versus the entry in the phone directory. You control the directory entry. The landlord controls the name plate.

They are supposed to match, and nothing forces them to. When a post office refuses to accept mail from a building with no name plate on it, that is exactly what a receiving mail server does to a host with no PTR record.

Reverse DNS answers "which name belongs to this address", and it works by a trick: the address is turned into a name inside a special zone, and then looked up like anything else.

plain text
8.8.8.8   ->   8.8.8.8.in-addr.arpa.      octets reversed, because DNS
                                          reads most-specific-first

dig -x 8.8.8.8 builds that name for you.

The thing nearly everyone gets wrong: forward and reverse are unrelated databases, controlled by different people. The forward record is controlled by whoever owns the domain. The PTR is controlled by whoever owns the IP address — your cloud provider or your ISP.

So you cannot create a PTR for a public IP you rent, unless the provider gives you a control for it. And there is no requirement that they agree: A and PTR can point at completely different things, both perfectly valid.

🧪 Exercise B7.1 — Reverse three addresses, including one that has nothing
bash
dig +short -x 8.8.8.8
dig +short -x 52.77.52.233
dig +short -x 14.99.30.4
Expected result — click to reveal
plain text
$ dig +short -x 8.8.8.8
dns.google.

$ dig +short -x 52.77.52.233
ec2-52-77-52-233.ap-southeast-1.compute.amazonaws.com.

$ dig +short -x 14.99.30.4
(no output)

Each of the three tells you something different, and none of them is about the domain you started from.

dns.google. — a deliberately curated PTR. Someone chose that name.

ec2-52-77-52-233.ap-southeast-1.compute.amazonaws.com. — an AWS default, and it leaks two facts for free: this is an EC2 instance, and it is in ap-southeast-1, Singapore. You learned the hosting provider and the region without any access to the account. Reverse DNS is one of the most under-used reconnaissance surfaces there is, and Part D uses it deliberately.

14.99.30.4 returns nothing at all — no PTR exists. Common for on-premises and colocated ranges where nobody configured the reverse zone.

Why the empty one is a real finding rather than cosmetic. Receiving mail servers routinely check that a sending IP has a PTR, and that the PTR resolves forward to the same address — the forward-confirmed reverse DNS check. A host with no PTR that tries to send mail gets a deliverability penalty or an outright rejection. If any of the SPF-listed sending ranges from B6 lack PTRs, that is a mail-deliverability problem waiting to be blamed on something else.


B8 · SRV — service discovery

The analogy. Think of the difference between an address and "third floor, counter 5". An ordinary address gets you to the right building and then you wander around looking for the right desk.

SRV gives you the building and the counter — it is the only common record that carries a port number. Which is why a service using it can move to a different counter without telling every visitor.

A tells you where a host is. SRV tells you where a service is, including its port — the only core record type that carries one.

plain text
_xmpp-client._tcp.jabber.org.  60  IN  SRV  30    30      5222  scarlet.jabber.org.
│            │                                │     │       │     └── target host
│            │                                │     │       └──────── port
│            │                                │     └──────────────── weight
│            │                                └────────────────────── priority
│            └───────────────────────────────────────────────────────  protocol, _tcp or _udp
└────────────────────────────────────────────────────────────────────  service name
Priority behaves like MX preference: lowest wins. Weight is the genuinely different part — among records of equal priority, clients are meant to distribute proportionally to weight. That makes SRV the only standard DNS record with real weighted load balancing built in.

The catch, and it is a big one: the client has to implement it. Browsers do not use SRV for HTTP at all. Where SRV does get used properly — Kubernetes headless services, XMPP, SIP, Active Directory, Minecraft — it works well, and Module 08 shows Kubernetes generating these automatically for every named port.

🧪 Exercise B8.1 — Read a live SRV record
bash
dig +noall +answer _xmpp-client._tcp.jabber.org SRV
Expected result — click to reveal
plain text
_xmpp-client._tcp.jabber.org. 60 IN	SRV	30 30 5222 scarlet.jabber.org.

Priority 30, weight 30, port 5222, target scarlet.jabber.org. A client that speaks XMPP needs no configuration beyond the domain name — it constructs _xmpp-client._tcp.jabber.org, and DNS hands back the host and the port.

Compare that with how HTTP works. There is no SRV for the web; the port is a convention baked into the scheme, 80 or 443. That is why moving a web service to a different port requires telling every client, while moving an XMPP or SIP service is a DNS change. It is a real architectural difference and it explains why service meshes and Kubernetes lean on SRV so heavily.

The naming rule to remember, because you will construct these by hand in Module 08: _service._proto.name, both underscore-prefixed, protocol is _tcp or _udp.


B9 · CAA — who is allowed to issue your certificates

The analogy. Think of leaving a note at the registry saying which solicitor is allowed to sign documents in your name. Nobody reads that note day to day. It is checked at exactly one moment: when somebody turns up wanting to issue a document as you.

With no note on file, every solicitor in the world qualifies. That is the default state of any domain without a CAA record.

A CAA record names the certificate authorities permitted to issue certificates for your domain. Certificate authorities are required to check it at issuance time.

CAA is a control that runs at issuance, not at connection. No browser ever reads it. It exists to stop a CA — any CA in the world, including one you have never heard of — from issuing a valid certificate for your domain because somebody convinced them they were you. Without CAA, every trusted CA on earth is authorised to issue for you by default.

It climbs the tree. A CA checking api.eng.example.com looks there, then at eng.example.com, then example.com, and uses the first CAA set it finds. So one record at the apex protects the whole zone.

🧪 Exercise B9.1 — Compare a domain that has CAA with one that does not
bash
dig +noall +answer google.com CAA
dig +noall +comments seamless.se CAA
Expected result — click to reveal
plain text
$ dig +noall +answer google.com CAA
google.com.		1341	IN	CAA	0 issue "pki.goog"

$ dig +noall +comments seamless.se CAA
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12903
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

Google's record says: only pki.goog may issue for this domain. The leading 0 is the flags byte; issue is the property tag.

seamless.se returns NODATA — no CAA record at all. Read that precisely, because the two readings are very different:

  • It does not mean certificate issuance is blocked
  • It means every publicly trusted CA in the world is permitted to issue for this domain, which is the default when no CAA record exists

This is a finding on nearly every domain audit, and it is one of the cheapest security improvements available: a single record, no user-visible change, no risk of breaking traffic, and it removes hundreds of organisations from the set of people who can mint a valid certificate for you.

The one way it bites you, and it is worth stating so you set it up correctly: add a CAA that names only your current CA, then later try to move to a different one, and the new CA will refuse to issue. The failure appears at renewal time, which is often 3am on a weekend. Add every CA you actually use, including the one your CDN or load balancer provisions certificates through, and use the iodef tag so violations are reported to you rather than silently dropped.

🎯 Interview questions — Record types

Q. Name the record types you use regularly and what each is for.

A and AAAA for IPv4 and IPv6 addresses. CNAME for aliases. NS for delegation. SOA for zone metadata and the negative caching TTL. MX for mail routing. TXT for SPF, DKIM, DMARC and ownership proofs. PTR for reverse lookups. SRV for service discovery including a port. CAA for certificate issuance policy.

What makes this answer stand out is grouping rather than listing. Three of them — NS, SOA and the delegation records — describe the zone itself. Three describe hosts. Three are policy records that no packet ever depends on but which break things badly when wrong: TXT for mail authentication, CAA for certificates, PTR for mail reputation. Reciting nine types is memorisation; naming three categories shows you understand what DNS is being used for.

Q. Which record type carries a port number?

SRV, and it is the only common one that does. Its RDATA is priority, weight, port and target, and the name is structured as _service._proto.name.

Priority works like MX preference — lowest wins — and weight distributes proportionally among equal priorities, which makes SRV the only core record type with genuine weighted load balancing.

The caveat that shows real-world experience: it only works if the client implements it, and most do not. Browsers ignore SRV entirely for HTTP. Where it is genuinely load-bearing is Kubernetes headless services, SIP, XMPP and Active Directory.


Part C · Zone files and wildcards

C1 · The zone file format

The analogy. Think of a form that auto-completes your address. You write "Flat 3" and the form quietly adds the street, city and country you set at the top.

Helpful — until you write out the full address by habit and the form appends the city again. You end up with something that looks perfectly reasonable, passes every check, and points nowhere.

A zone file is the text representation of a zone. Managed providers hide it behind a web form, but the format is what their API is a wrapper around, and it is what BIND reads directly in Module 05. Four constructs do all the work.

ConstructMeaning
$ORIGIN name.The suffix appended to every relative name below this line. Usually the zone name
$TTL secondsThe default TTL for records that do not state one. Required, or you get warnings and a guess
@Shorthand for the current $ORIGIN — i.e. the zone apex
Blank owner nameA record whose first column is whitespace inherits the previous record's owner name
The rule that causes more broken zones than anything else, and you were warned about it in Module 01 A3. Inside a zone file, any name that does not end in a dot gets $ORIGIN appended to it.

So writing www.example.com — which looks completely correct — inside the example.com zone produces www.example.com.example.com.

The file loads. There is no error. Nothing warns you. You get a valid zone containing a name nobody will ever query, and the name you meant to create simply does not exist.

The habit that removes the problem entirely: write www, or write www.example.com. with the dot. Never write the half-qualified middle form.

Here is a complete, valid zone with every construct in it:

plain text
$TTL 3600
$ORIGIN example.internal.
@       IN  SOA ns1.example.internal. hostmaster.example.internal. (
                2026081701  ; serial
                7200        ; refresh
                900         ; retry
                1209600     ; expire
                900 )       ; minimum - negative TTL
@       IN  NS  ns1.example.internal.
@       IN  NS  ns2.example.internal.
ns1     IN  A   192.0.2.10
ns2     IN  A   192.0.2.11
@       IN  A   192.0.2.20
www     IN  CNAME @
api     IN  A   192.0.2.30
api     IN  A   192.0.2.31
@       IN  MX  10 mail.example.internal.
mail    IN  A   192.0.2.40
*       IN  A   192.0.2.99

Read three things out of that file before you run anything. api has two A records — that is one RRset with two members, exactly as A1 described, and they will share the $TTL of 3600. www is a CNAME to @, which is the apex — legal, because the alias is at www, not at the apex. And the last line begins with *, which is a wildcard; C4 is entirely about why that line is more dangerous than it looks.


C2 · Writing and validating a zone by hand

The analogy. Think of the difference between spell-check and a proofreader. Spell-check confirms every word is a real word. It will happily approve a sentence that means completely the wrong thing.

named-checkzone is spell-check. The -D dump is the proofreader — it reads the file back to you as the server understood it, so you can see what you actually said.

You do not need a running nameserver to check a zone file. named-checkzone parses it exactly as named would.

bash
sudo apt install -y bind9-utils      # Debian / Ubuntu
sudo dnf install -y bind-utils       # RHEL / Rocky / Fedora
🧪 Exercise C2.1 — Write the zone, validate it, then read it back as the server sees it
bash
mkdir -p ~/dns-lab && cd ~/dns-lab
# paste the zone from C1 into db.example.internal, then:
named-checkzone example.internal db.example.internal

# -D dumps the zone in canonical form: every name fully qualified,
# every TTL explicit, every shorthand expanded
named-checkzone -D example.internal db.example.internal
Expected result — click to reveal
plain text
$ named-checkzone example.internal db.example.internal
zone example.internal/IN: loaded serial 2026081701
OK

$ named-checkzone -D example.internal db.example.internal
zone example.internal/IN: loaded serial 2026081701
example.internal.	3600 IN SOA	ns1.example.internal. hostmaster.example.internal. 2026081701 7200 900 1209600 900
example.internal.	3600 IN NS	ns1.example.internal.
example.internal.	3600 IN NS	ns2.example.internal.
example.internal.	3600 IN A	192.0.2.20
example.internal.	3600 IN MX	10 mail.example.internal.
*.example.internal.	3600 IN	A	192.0.2.99
api.example.internal.	3600 IN	A	192.0.2.30
api.example.internal.	3600 IN	A	192.0.2.31
mail.example.internal.	3600 IN	A	192.0.2.40
ns1.example.internal.	3600 IN	A	192.0.2.10
ns2.example.internal.	3600 IN	A	192.0.2.11
www.example.internal.	3600 IN	CNAME	example.internal.
OK

-D is the most under-used debugging tool in DNS and you should build a habit around it. It shows you the zone as the server understands it, with every @ expanded, every relative name qualified, and every inherited TTL made explicit.

Which means every ambiguity in your file becomes visible. www IN CNAME @ became www.example.internal. CNAME example.internal. — you can see with your own eyes that the @ resolved to what you intended. And if you had made the trailing-dot mistake, you would see the doubled name sitting right there in the output instead of discovering it from a user report.

The workflow to adopt: edit, named-checkzone, then named-checkzone -D and actually read it. OK only tells you the file parses. The dump tells you it means what you meant.


C3 · CNAME at the apex, and what your provider does instead

The analogy. Think of your company's registered head office address. You cannot file a "forward all post from here" order on it, because that is the address the tax office and the bank must be able to write to.

So providers offer a workaround: they quietly do the forwarding behind the counter and hand out a normal address, so from the outside nothing unusual is happening at all.

B2 told you the rule and the reason. Now watch the server enforce it.

🧪 Exercise C3.1 — Put a CNAME at the apex and get the zone rejected
bash
cd ~/dns-lab
# replace the apex A record with a CNAME
sed 's|^@       IN  A   192.0.2.20|@       IN  CNAME cdn.provider.net.|' \
    db.example.internal > broken-apex.zone

named-checkzone example.internal broken-apex.zone
Expected result — click to reveal
plain text
dns_master_load: broken-apex.zone:13: example.internal: CNAME and other data
dns_master_load: broken-apex.zone:13: example.internal: CNAME and other data
dns_master_load: broken-apex.zone:17: example.internal: CNAME and other data
zone example.internal/IN: loading from master file broken-apex.zone failed: CNAME and other data
zone example.internal/IN: not loaded due to errors.

CNAME and other data — memorise that string. It is the error you will meet whenever a name has a CNAME plus anything else, and BIND reports it three times because three separate records collide with the alias: the two NS records at line 13, and the MX at line 17.

The zone is not loaded. This is a good failure: loud, immediate, and before anything is served. Compare it with the failures in C5 that load cleanly and break in production.

Now the useful part — what you actually do when a vendor says "point your root domain at this hostname". You cannot use a CNAME, so every provider offers a non-standard pseudo-record:

ProviderWhat it is called
Route 53alias record
CloudflareCNAME flattening
DNSimple, easyDNSALIAS
DNS Made Easy, othersANAME

All of them work the same way, and understanding the mechanism tells you the trade-off. They are not on the wire. The authoritative server resolves the target internally and answers your query with a genuine A or AAAA record. Standards compliance is preserved because a normal address record is what leaves the server.

The two consequences that bite. First, the flattening happens from the provider's network position, not yours — so if the target is geo-distributed, your users may get an address chosen for the wrong region. Second, these records are provider-specific and cannot be transferred, so migrating DNS providers means re-creating every one of them by hand, and forgetting one is an apex outage.


C4 · Wildcards

The analogy. Think of telling the postman "anything addressed to this building, just leave it with the concierge". Convenient. But now nothing is ever returned as "no such person" — including letters for people who have never worked there.

You have quietly given up your ability to detect mistakes. Every typo now succeeds, and every check that relied on "this name does not exist" is broken forever.

A record whose owner name is * matches any name at that level that does not otherwise exist.

Four properties, and every wildcard surprise comes from one of them.
  1. It only fires when nothing else matches. If api.example.com exists, the wildcard is never consulted for it — not even for a record type the real name lacks. Existence of the name blocks the wildcard, not existence of the record type
  2. It matches one level only. *.example.com matches foo.example.com but not foo.bar.example.com
  3. It is type-specific. * IN A answers A queries. A query for MX at a name covered by that wildcard returns NODATA, not the wildcard
  4. The name in the answer is the queried name, not *. Nothing in the response reveals that a wildcard produced it
Property 4 is why wildcards are operationally hostile: they abolish NXDOMAIN. With a wildcard in place, every name under the zone answers successfully. Typos resolve. Decommissioned services resolve. Names nobody ever created resolve.

You lose your single most reliable signal — "this name does not exist" — for every name in the zone. Monitoring that checks "does this record resolve" becomes worthless, and Part D's enumeration techniques stop working entirely against that zone.

🧪 Exercise C4.1 — Prove all four properties in one pass
bash
cd ~/dns-lab
named-checkzone -D example.internal db.example.internal | grep -E '^\*|^api|^www'

Then reason it through before opening the toggle. In the zone from C1, which of these return the wildcard address 192.0.2.99?

plain text
1. anything.example.internal        A
2. api.example.internal             A
3. api.example.internal             MX
4. deep.nested.example.internal     A
5. www.example.internal             A
Expected result — click to reveal
plain text
*.example.internal.	3600 IN	A	192.0.2.99
api.example.internal.	3600 IN	A	192.0.2.30
api.example.internal.	3600 IN	A	192.0.2.31
www.example.internal.	3600 IN	CNAME	example.internal.

1. anything.example.internal → 192.0.2.99. YES. No such name exists, so the wildcard fires. This is the case everyone expects.

2. api.example.internal → 192.0.2.30 and .31. NO. The name exists with its own A records, so the wildcard is never reached.

3. api.example.internal MX → NODATA. NO — and this is the one that catches people. api has no MX record, so you might expect the wildcard to supply one. It does not. The name api exists, and existence blocks the wildcard for every type. You get NOERROR with ANSWER: 0. Property 1, stated precisely.

4. deep.nested.example.internal → NXDOMAIN. NO. *.example.internal covers one label only. A wildcard does not descend, and *.*.example.internal is not a thing.

5. www.example.internal → follows the CNAME to the apex, 192.0.2.20. NO. www exists as an alias, and that is enough to block the wildcard.

The mental model that gets all five right without memorising anything: the resolver looks for the exact name first. If the name exists in any form at all, the wildcard is out of the picture — regardless of which type you asked for. Only when the name is entirely absent does the wildcard get consulted, and then only for the type you asked for, and only one level down.

Now imagine this at 500 hosts. Someone adds *.example.com A pointing at a marketing landing page. Every mistyped internal hostname now resolves to the marketing site. Health checks that relied on NXDOMAIN to detect a decommissioned service go green forever. And the zone becomes un-auditable, because you can no longer distinguish a name that exists from one that does not.


C5 · The mistakes that load cleanly and still break

The analogy. Think of a form filled in neatly, in the right boxes, with the wrong details. Nobody hands it back. There is no red pen. It goes straight into the system and quietly does the wrong thing for months.

The errors that cost you a weekend are never the ones that get rejected at the counter.

C3's failure was the easy kind: loud, immediate, nothing served. These three are the dangerous kind. All three produce a zone that loads successfully.

🧪 Exercise C5.1 — Commit three silent errors on purpose and see how quiet they are
bash
cd ~/dns-lab

echo "===== 1. two TTLs in one RRset ====="
sed 's|^api     IN  A   192.0.2.31|api  60  IN  A   192.0.2.31|' \
    db.example.internal > b-ttl.zone
named-checkzone example.internal b-ttl.zone

echo "===== 2. MX pointing at a CNAME ====="
sed 's|^@       IN  MX  10 mail.example.internal.|@       IN  MX  10 www.example.internal.|' \
    db.example.internal > b-mx.zone
named-checkzone example.internal b-mx.zone

echo "===== 3. the missing trailing dot ====="
sed 's|^www     IN  CNAME @|www     IN  CNAME www.example.internal|' \
    db.example.internal > b-dot.zone
named-checkzone -D example.internal b-dot.zone | grep www
Expected result — click to reveal
plain text
===== 1. two TTLs in one RRset =====
b-ttl.zone:16: TTL set to prior TTL (3600)
zone example.internal/IN: loaded serial 2026081701
OK

===== 2. MX pointing at a CNAME =====
zone example.internal/IN: example.internal/MX 'www.example.internal' is a CNAME (illegal)
zone example.internal/IN: loaded serial 2026081701
OK

===== 3. the missing trailing dot =====
www.example.internal.			      3600 IN CNAME	www.example.internal.example.internal.

Look at the last line of the first two: OK. Both zones loaded and would be served.

Error 1 — the TTL you wrote was thrown away. You asked for 60 seconds on one address; BIND silently forced it back to 3600 because RFC 2181 forbids differing TTLs inside an RRset. One line of warning in a log nobody reads, and then it serves happily. If that 60-second TTL was your failover plan, your failover plan is now an hour long and you have no idea.

Error 2 — is a CNAME (illegal), followed by OK. The zone loads with a mail configuration that is invalid per RFC 2181. Some senders will follow it, some will refuse. You get partial mail delivery, which is far harder to diagnose than none at all, because the people who can reach you have no reason to report anything.

Error 3 — no warning whatsoever, and the damage is right there in the dump. www.example.internal. is now an alias for www.example.internal.example.internal., a name that does not exist. named-checkzone said OK because the file is syntactically perfect. This is the trailing-dot bug from Module 01 A3, and it is the reason C2 tells you to read the -D dump rather than trust the word OK.

The general lesson, which is worth more than the three specifics. In DNS, OK means "this parses", not "this is correct". The failures that cost you a weekend are never syntax errors — they are semantically valid files that say something you did not mean. That is why the verification step is always query the result, never the file loaded.

🎯 Interview questions — Zone files

Q. What happens if you forget the trailing dot in a zone file?

The zone's $ORIGIN is appended, so www.example.com written inside the example.com zone becomes www.example.com.example.com.

The file is syntactically valid, so it loads without error and named-checkzone reports OK. You get a working zone containing a name nobody will ever query, and the name you intended does not exist.

How I avoid it, which is the practical half of the answer: write names either fully relative — www — or fully qualified with the dot, and never the half-qualified middle form. Then verify with named-checkzone -D, which dumps every name fully expanded so the doubled name is visible immediately rather than discovered from a user report.

Q. A colleague adds *.example.com A 192.0.2.99. What are your concerns?

The main one is that NXDOMAIN disappears from the entire zone. Every conceivable name now resolves, so typos succeed, decommissioned services keep answering, and any monitoring that relies on a name failing to resolve goes permanently green.

I would also check what it shadows and what it does not: the wildcard only fires for names that do not otherwise exist, matches a single label, and is type-specific — so api.example.com MX still returns NODATA if api exists with only an A record.

And the operational point I would raise in the review: it makes the zone un-auditable. You can no longer answer "does this name exist" for anything under the domain, which breaks inventory, breaks certificate scoping, and breaks the enumeration techniques you would otherwise use to find out what you own.


Part D · Field recipes — a domain you have been handed

What this Part is. Everything above was the theory. This is the working procedure for the request you will actually receive: "here is a domain — tell me what exists under it, and where it points."

Every command here has been run against seamless.se and the outputs are real. Substitute your own domain and the recipes work unchanged.

Scope, before anything else. Enumerating names under a domain is ordinary asset-inventory work on domains your organisation owns, or that you are explicitly authorised to assess. Every technique below is read-only and uses public data, but pointing them at somebody else's estate is reconnaissance, not administration. Get the authorisation in writing before you run the wide sweeps in D3 — and note that D3's dictionary probing generates enough query volume to show up in a target's DNS logs.

D1 · What records does this one name have?

The analogy. Think of emptying your pockets one at a time. There is no single move that shows you everything, so you go through them in order and note what is in each — including the empty ones, because "nothing in this pocket" is information too.

There is no listing operation, so "what records does this name have" is always a loop over types. There are three ways to write that loop and it is worth knowing all three.

bash
DOMAIN=seamless.se

# 1. the shell loop - clearest, and lets you label each type
for t in SOA NS A AAAA MX TXT CAA SRV DNSKEY; do
  dig +noall +answer "$DOMAIN" "$t"
done

# 2. several queries in ONE dig invocation - fewer processes
dig +noall +answer $DOMAIN A $DOMAIN MX $DOMAIN TXT $DOMAIN NS

# 3. batch mode from a file - best for many names at once
printf '%s A\n%s MX\n%s NS\n' $DOMAIN $DOMAIN $DOMAIN > /tmp/q.txt
dig -f /tmp/q.txt +noall +answer
🧪 Exercise D1.1 — Run the full sweep on a real domain and read the findings
bash
DOMAIN=seamless.se
for t in SOA NS A AAAA MX TXT CAA DNSKEY; do
  out=$(dig +noall +answer "$DOMAIN" "$t")
  [ -n "$out" ] && echo "$out" || printf '%-8s  (none)\n' "$t"
done
Expected result — click to reveal
plain text
seamless.se.		900	IN	SOA	ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400
seamless.se.		3600	IN	NS	ns-135.awsdns-16.com.
seamless.se.		3600	IN	NS	ns-1490.awsdns-58.org.
seamless.se.		3600	IN	NS	ns-1760.awsdns-28.co.uk.
seamless.se.		3600	IN	NS	ns-722.awsdns-26.net.
seamless.se.		60	IN	A	52.77.52.233
AAAA      (none)
seamless.se.		3600	IN	MX	10 aspmx.l.google.com.
seamless.se.		3600	IN	MX	20 alt1.aspmx.l.google.com.
seamless.se.		3600	IN	MX	20 alt2.aspmx.l.google.com.
seamless.se.		3600	IN	MX	30 aspmx2.googlemail.com.
seamless.se.		3600	IN	MX	30 aspmx3.googlemail.com.
seamless.se.		300	IN	TXT	"v=spf1 include:amazonses.com include:_spf.linserv.se include:spf.mandrillapp.com include:_spf.salesforce.com a:mail.workbuster.se include:_spf.google.com ip4:3.1.214.242 ip4:14.142.43.230 ip4:14.99.30.2 ~all"
CAA       (none)
DNSKEY    (none)

The (none) lines are worth as much as the records, and this is the habit to build: print the absences explicitly. A sweep that only shows what exists lets you skim past three findings.

The seven-line briefing you can now give, without asking anyone anything:

  1. DNS is on AWS Route 53 — the awsdns nameservers. You know which console and which API
  2. Mail is Google Workspaceaspmx.l.google.com, with a proper three-tier preference ladder
  3. No IPv6. IPv6-only clients cannot reach this name
  4. No CAA — every publicly trusted CA in the world may issue certificates for this domain
  5. No DNSKEY — the zone is not DNSSEC-signed. That matters practically in D3: it rules out one enumeration technique entirely
  6. The A TTL is 60 while everything else is 3600 — this address is designed to be moved quickly. Ask why
  7. The SPF record names six external senders and three ip4: addresses. Six vendor relationships, and three IPs that belong to the organisation's own mail infrastructure — keep those, D4 uses them

Now imagine this at 500 domains. This is exactly why it becomes the script in D5 rather than something you type.


D2 · Does this specific name exist?

The analogy. Think of checking whether someone still works at a company. Asking the person at the next desk gets you their memory of last year. Asking HR gets you the truth.

A resolver is the colleague; the authoritative server is HR. And before you trust any answer, check the company is not one of those places where reception says "yes, they work here" about absolutely everybody — that is the wildcard check.

This is your admin-vodafone.seamless.se question, and it has a precise procedure — because "it didn't return an IP" has four different meanings, and only one of them means the name is absent.

Diagram source
flowchart TD
    Q["dig name +noall +comments"] --> S{"read status:"}
    S -->|"NOERROR<br>ANSWER greater than 0"| E["EXISTS<br>and has this record type"]
    S -->|"NOERROR<br>ANSWER 0"| N["EXISTS<br>but no record of THIS type<br>try A, AAAA, CNAME, TXT"]
    S -->|"NXDOMAIN"| X{"does the zone<br>have a wildcard?"}
    S -->|"SERVFAIL or REFUSED"| U["UNKNOWN<br>you learned nothing<br>ask another resolver"]
    X -->|"no wildcard"| G["DOES NOT EXIST<br>this is a real answer"]
    X -->|"wildcard present"| W["cannot conclude<br>NXDOMAIN should be<br>impossible - investigate"]
    style E fill:#22c55e,color:#fff
    style N fill:#f59e0b,color:#fff
    style G fill:#22c55e,color:#fff
    style U fill:#ef4444,color:#fff
    style W fill:#ef4444,color:#fff
Always check for a wildcard before you trust an existence result. It costs one query and it decides whether your answer means anything. Query a name nobody could have created — random characters — and see what comes back. NXDOMAIN means the zone has no wildcard and existence checks are meaningful. Anything else means every name in the zone "exists" and enumeration is unreliable.
🧪 Exercise D2.1 — Answer the real question: does admin-vodafone.seamless.se exist?
bash
# step 1 - is there a wildcard? if there is, nothing below means anything
dig zz9x7q2w-nonexistent-probe.seamless.se +noall +comments

# step 2 - the name itself, with the status visible
dig admin-vodafone.seamless.se +noall +comments +answer

# step 3 - other types, in case step 2 was NODATA
dig admin-vodafone.seamless.se AAAA +short
dig admin-vodafone.seamless.se CNAME +short

# step 4 - confirm against an authoritative server, no cache involved
dig @ns-135.awsdns-16.com admin-vodafone.seamless.se +noall +comments +answer
Expected result — click to reveal
plain text
$ dig zz9x7q2w-nonexistent-probe.seamless.se +noall +comments
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 44544
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

$ dig admin-vodafone.seamless.se +noall +comments +answer
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61804
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

admin-vodafone.seamless.se. 30	IN	A	77.83.61.131

$ dig admin-vodafone.seamless.se AAAA +short
(no output)

$ dig admin-vodafone.seamless.se CNAME +short
(no output)

$ dig @ns-135.awsdns-16.com admin-vodafone.seamless.se +noall +comments +answer
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35503
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

admin-vodafone.seamless.se. 30	IN	A	77.83.61.131

The answer: yes, it exists, and it is 77.83.61.131. Now the reasoning, because the reasoning is the transferable part.

Step 1 established the result is trustworthy. A random name returned NXDOMAIN, so there is no wildcard in this zone. Had it returned an address, every subsequent check would have been meaningless and you would need a completely different approach.

Step 2 gave NOERROR with ANSWER: 1. Existence confirmed, plus the address.

Step 3 says the name has an A record and nothing else — no IPv6, not an alias. Worth running because a name that returns nothing for A can still exist as a CNAME or with only a TXT, and stopping at step 2 would have you report "does not exist" for a name that does.

Step 4 is the one people skip, and it is the one that makes the answer defensible. Note aa in the flags and the TTL at the full 30 rather than a counted-down value. This came from Route 53 itself, not from a cache. Step 2 tells you what a resolver believes; step 4 tells you what the zone actually says. When those two disagree you have found a stale cache, and that is a completely different investigation.

Two more things this output tells you for free.

The TTL is 30 seconds — even shorter than the apex's 60. Somebody wants to move this address fast. On a customer-named host like this one, that usually means a manually operated failover.

77.83.61.131 is nowhere near the other addresses in this domain — not the AWS Singapore range, not the on-premises ranges. A separate hosting arrangement, which for a customer-specific host is exactly what you would expect and exactly what you should confirm with whoever owns it.

🎯 Interview questions — Proving existence

Q. How do you determine whether a given hostname exists?

I query it and read status: rather than looking for an IP address. NOERROR with a non-zero answer count means it exists with that type; NOERROR with zero answers means the name exists but has no record of that type; NXDOMAIN means no such name; SERVFAIL or REFUSED means I learned nothing and must ask elsewhere.

I check more than one type, because a name can exist as a CNAME or with only a TXT record and still return nothing for A.

The two steps that make the answer defensible, and that most candidates omit. First, probe a random name in the same zone to check for a wildcard — with one present, every name "exists" and the whole exercise is void. Second, confirm against an authoritative server rather than a resolver, so a cached negative answer cannot masquerade as a missing record.


D3 · You cannot list a zone. Here are the six ways people do it anyway

The analogy. Think of being asked for a company's full staff list when nobody will hand you one. You can ask about one person at a time, and that is all.

So you reconstruct it: ask HR if you have access (the only complete answer); see who has been issued a visitor badge; guess the common names; and read the names that turn up in other paperwork. Every method except HR gives you a partial list, and you must say so when you hand it over.

Say it once more, because every failed attempt at this comes from forgetting it: the DNS protocol has no operation that returns the contents of a zone. You can ask "is there an A record at this exact name" and nothing else. Enumeration is therefore always indirect — you are reconstructing a list from sources that happen to leak it.

MethodCompletenessWhen it works
1. The provider's APITotalAlways, if you have credentials. The only method that is actually complete
2. Zone transfer (AXFR)TotalAlmost never permitted. When it is, that is itself the finding
3. Certificate TransparencyPartialAny name that has ever had a public TLS certificate. Very effective
4. Dictionary probingPartialAlways available. Finds the conventional names, which is most of them
5. NSEC / NSEC3 walkingTotalOnly on DNSSEC-signed zones. Module 07
6. Your own records leakingPartialAlways. SPF, MX, NS and reverse DNS all name infrastructure
The ordering that saves you time. If it is your own domain, method 1 ends the exercise — read the zone from Route 53, Cloudflare or wherever it lives, and everything else is unnecessary. The other five exist for the cases where you cannot: an estate whose credentials are lost, a subsidiary nobody documented, a supplier you are assessing, or an incident at 3am when nobody with console access is awake.
🧪 Exercise D3.1 — Try the zone transfer, and understand why it fails
bash
for ns in $(dig +short seamless.se NS); do
  echo "--- $ns ---"
  dig @"$ns" seamless.se AXFR +time=3 +tries=1 +noall +comments 2>&1 | tail -3
done
Expected result — click to reveal
plain text
--- ns-1490.awsdns-58.org. ---
;; Connection to 205.251.192.135#53(205.251.192.135) for seamless.se failed: timed out.
;; no servers could be reached

--- ns-722.awsdns-26.net. ---
;; Connection to 205.251.194.210#53(205.251.194.210) for seamless.se failed: timed out.
;; no servers could be reached

All four time out, and that is the correct, healthy result. Route 53 does not offer AXFR to the public at all.

Read the mechanics, because they matter. AXFR is a TCP query — a whole zone will not fit in a UDP datagram — so dig opens a TCP connection to port 53 and the server declines to complete it. On a BIND server with allow-transfer restricted you would instead get a clean Transfer failed with a REFUSED, which is a more informative rejection.

When this succeeds, stop and write it up. An open AXFR hands a stranger every name in your zone: internal hostnames, staging systems, backup servers, management interfaces, the lot. It is a decades-old audit finding and it still appears, usually on a forgotten secondary rather than the primary — which is why the loop above tries every nameserver rather than just the first.

And this is why the D5 script tries all of them and prints a finding when one answers. You are not expecting success; you are checking that the answer is "no" everywhere.

🧪 Exercise D3.2 — Certificate Transparency: names that a dictionary would never find
bash
# Every publicly trusted certificate is logged, and every certificate
# names the hostnames it covers. That makes CT a searchable index of
# names that have ever been given a certificate.
curl -s 'https://crt.sh/?q=%25.seamless.se&output=json' \
  | jq -r '.[].name_value' \
  | tr '\n' '\n' | sed 's/^\*\.//' | sort -u
Expected result — click to reveal

Run this one yourself — the log is appended to constantly and any output pasted here would be wrong within a week. What matters is how to read it, and that is the same every time.

The output is one hostname per line, deduplicated, with wildcard prefixes stripped. Expect it to be noisy in three specific ways, and expect each kind of noise to be informative:

  • Names that no longer resolve. A certificate was issued once and the name has since been retired. These are not junk — they are a history of what the estate used to run, and retired names sometimes come back pointed at somebody else's infrastructure
  • Wildcard entries such as *.seamless.se. That tells you a wildcard certificate exists, which is a different thing from a wildcard DNS record, and it means any subdomain can present a valid certificate
  • Names created for a single purpose — one-off customer environments, short-lived demos. These are exactly the names a dictionary never finds, and they are the reason to run this at all

The essential step is to feed every name back through dig and keep the ones that still resolve. CT tells you what was certificated; only DNS tells you what is live. The two lists overlap and neither contains the other.

The blind spot to be honest about. CT only sees names that were given a publicly trusted certificate. Anything on an internal CA, anything served over plain HTTP, and anything that is not a web service at all — a database host, an SSH bastion, a mail relay — is invisible here. Those are often precisely the hosts you most wanted to inventory.

If curl and jq are not installed: sudo apt install -y curl jq.

🧪 Exercise D3.3 — Dictionary probing, and reading the result as an architecture
bash
DOMAIN=seamless.se
for n in www mail smtp dev demo jira confluence support crm gw intranet sds api admin vpn; do
  ans=$(dig +noall +answer "$n.$DOMAIN" A)
  [ -n "$ans" ] && echo "$ans"
done
Expected result — click to reveal
plain text
www.seamless.se.	3600	IN	CNAME	seamless.se.
seamless.se.		60	IN	A	52.77.52.233
mail.seamless.se.	133	IN	CNAME	ghs.googlehosted.com.
ghs.googlehosted.com.	69	IN	A	192.178.129.121
smtp.seamless.se.	3600	IN	CNAME	aspmx.l.google.com.
aspmx.l.google.com.	293	IN	A	64.233.181.26
dev.seamless.se.	133	IN	A	52.76.82.127
demo.seamless.se.	3600	IN	A	195.22.81.226
jira.seamless.se.	300	IN	A	14.99.30.4
confluence.seamless.se.	300	IN	A	14.99.30.4
support.seamless.se.	300	IN	A	14.99.30.4
crm.seamless.se.	300	IN	A	14.142.42.18
gw.seamless.se.		3600	IN	A	195.22.81.194
intranet.seamless.se.	3600	IN	CNAME	ghs.google.com.
sds.seamless.se.	3600	IN	CNAME	seamless.se.

Twelve names found from fifteen guesses, and api, admin and vpn returned nothing at all. Because D2 already proved there is no wildcard, those three absences are real information, not an inconclusive result.

But the list of names is the boring half. Read the addresses and an architecture appears:

Four distinct hosting environments, visible in the IP ranges alone:

  • 52.77.x / 52.76.x — AWS Singapore. The public site and dev
  • 14.99.30.4 and 14.142.42.18 — a completely different range hosting Jira, Confluence, support and CRM. On-premises or colocated
  • 195.22.81.x — a third range, with demo and gw
  • Googlemail, smtp and intranet are all CNAMEs into Google's infrastructure

Now the finding that is worth more than the inventory. jira, confluence and support are all on 14.99.30.4 — one single address. Three business-critical internal services share one host or one ingress point. That is a concentration of risk nobody chose deliberately; it accumulated. You cannot see it by reading a list of names, only by sorting the list by address — which is exactly what D4 does.

And connect it back to D1. The SPF record listed ip4:14.99.30.2 and ip4:14.142.43.230. Those are neighbours of the addresses you just discovered. The organisation's own SPF record told you which IP ranges to look at, before you probed anything. That is method 6 in the table, and it is free.

For real work use a proper tool rather than a shell loop — dnsx, massdns or dnsrecon with a wordlist such as SecLists. They run thousands of names concurrently and handle rate limiting. The loop above is here so you can see exactly what those tools do, which is all they do.

The failure mode that will make you hand your manager a wrong list — and it happened while writing this module.

A 485-word sweep run at 40-way concurrency against a public resolver returned 75 "hits". Re-checked one at a time, only 18 were real. zoom, wiki, login, vpn, lan and dozens more had been returning NXDOMAIN all along.

The cause was rate limiting. Public resolvers throttle bursts, and under throttling you get truncated, malformed or empty responses — so a naive script that tests "did dig print anything?" counts those as existence. The false positives clustered alphabetically from the middle of the list onward, which is the tell: the resolver began throttling partway through.

Two defences, and you need both.

  1. Test status:, never output length. Accept a name only when status: NOERROR and the answer section contains an actual A or CNAME line. [ -n "$output" ] is not an existence test
  2. Keep concurrency low — around 8 parallel queries with +tries=2, and re-verify every hit serially before it goes into a report

Why this matters more than it sounds. A false positive in a subdomain inventory is worse than a miss: somebody opens a firewall rule, buys a certificate, or writes a monitoring check for a host that has never existed. Always re-verify hits one at a time before the list leaves your hands.

Method 5, and why it does not apply here. On a DNSSEC-signed zone, the records that prove non-existence also reveal the next name in the zone — so you can walk from one to the next and enumerate the entire zone with no guessing at all. That is NSEC walking, and NSEC3 was designed to make it harder rather than impossible.

seamless.se returned no DNSKEY record in D1, so the zone is unsigned and this technique is unavailable. Module 07 covers it properly, including why "our zone is signed" and "our zone is enumerable" can be the same sentence.


D4 · From names to serving addresses — and the shared-IP trap

The analogy. Think of re-sorting the staff list by desk instead of by name. Sorted by name, nothing stands out. Sorted by desk, you suddenly notice that three people you cannot afford to lose are all sitting at the same one.

That finding was in the data the whole time and invisible until you changed the sort order.

A list of names is an inventory. A list of names sorted by address is an architecture diagram, and it answers questions the name list cannot: what is co-located, what is single-homed, and what is somewhere you did not expect.

🧪 Exercise D4.1 — Invert the list, then reverse-resolve every address
bash
DOMAIN=seamless.se
NAMES="www mail smtp dev demo jira confluence support crm gw intranet sds"

# names grouped by the address they serve
for n in $NAMES; do
  for ip in $(dig +short "$n.$DOMAIN" A | grep -E '^[0-9]+\.'); do
    printf '%-16s %s\n' "$ip" "$n.$DOMAIN"
  done
done | sort -V

echo "--- reverse DNS of each distinct address ---"
for n in $NAMES; do dig +short "$n.$DOMAIN" A; done \
  | grep -E '^[0-9]+\.' | sort -u | while read -r ip; do
      printf '%-16s %s\n' "$ip" "$(dig +short -x "$ip" | head -1)"
    done
Expected result — click to reveal
plain text
14.99.30.4       jira.seamless.se
14.99.30.4       confluence.seamless.se
14.99.30.4       support.seamless.se
14.142.42.18     crm.seamless.se
52.76.82.127     dev.seamless.se
52.77.52.233     www.seamless.se
52.77.52.233     sds.seamless.se
195.22.81.194    gw.seamless.se
195.22.81.226    demo.seamless.se

--- reverse DNS of each distinct address ---
14.142.42.18     
14.99.30.4       
52.76.82.127     ec2-52-76-82-127.ap-southeast-1.compute.amazonaws.com.
52.77.52.233     ec2-52-77-52-233.ap-southeast-1.compute.amazonaws.com.
64.233.181.121   ir-in-f121.1e100.net.
195.22.81.194    gw.seamless.se.
195.22.81.226    

The sorted view makes three things visible that the name list hid completely.

1. 14.99.30.4 serves three business-critical services. Jira, Confluence and the support system are one host. One reboot, one certificate, one firewall rule, one failure. This is the finding of the whole exercise and it is invisible until you sort by address.

2. www and sds are the same address — both aliases to the apex. Consistent, and it tells you sds is not a separate system but another name for the main site.

3. Four independent hosting environments. AWS Singapore, two on-premises ranges, and Google. Each one is a different operational contract, a different change process, and a different person to call.

Now the reverse DNS column, which is a health check in its own right:

  • gw.seamless.se reverses cleanly to gw.seamless.se. — forward and reverse agree. This is forward-confirmed reverse DNS and it is what a receiving mail server wants to see
  • 14.99.30.4, 14.142.42.18 and 195.22.81.226 have no PTR at all. If any of them ever sends mail, it will be penalised or rejected on reputation grounds. Cross-check against the SPF record from D1 — ip4:14.99.30.2 and ip4:14.142.43.230 are in exactly these ranges
  • The AWS defaults leak the regionap-southeast-1 — for free

The one trap that will make you wrong: a shared address does not always mean a shared host. Behind a CDN, a reverse proxy, a Kubernetes ingress or a cloud load balancer, hundreds of unrelated names legitimately share one address, and the co-location you "found" is an artefact. Before concluding anything, check whether the address belongs to a CDN or load balancer — reverse DNS usually tells you, since a CDN PTR is unmistakable. The 14.99.30.4 finding above holds because the reverse is empty and the range is not a CDN's, which is the sort of qualification that turns a guess into a finding.

🎯 Interview questions — Inventory and enumeration

Q. You have been given a domain and asked to find every subdomain. How do you approach it?

I start by saying that DNS has no listing operation, so this is always reconstruction from indirect sources, and the completeness depends entirely on which sources are available to me.

If it is our own domain, I read the zone from the provider's API or console — that is the only complete answer and it ends the exercise. If not: attempt a zone transfer against every nameserver, since one forgotten secondary allowing AXFR gives you the whole zone; query Certificate Transparency logs, which index every name that has ever had a publicly trusted certificate; run a dictionary sweep with a tool like dnsx or massdns; and if the zone is DNSSEC-signed, NSEC walking gives complete enumeration with no guessing.

I would also mine the organisation's own records — SPF ip4: mechanisms, MX targets and NS hostnames all name infrastructure, and reverse DNS across the ranges they reveal finds hosts a dictionary never would.

Two things I would state before starting, and they are what mark out someone who has done this professionally. First, authorisation in writing, since a wide sweep is indistinguishable from reconnaissance in a target's logs. Second, check for a wildcard first — with one present every name resolves, every negative result is meaningless, and the entire exercise has to be approached differently.

Q. Why is an open zone transfer a security finding?

Because AXFR returns the complete contents of the zone to whoever asks. That is every internal hostname, every staging and test system, every management interface, every backup host — a free map of the estate, and a target list.

It is not a vulnerability in DNS; it is a configuration that was appropriate for a secondary and got left open to the world. allow-transfer restricted to the secondaries' addresses, ideally with TSIG authentication, is the fix.

The detail that shows field experience: when it appears, it is almost never on the primary. It is on a secondary that someone stood up years ago and nobody has thought about since — which is why you test every nameserver in the NS set, not just the first one your tool picks.


D5 · The reusable audit script

The analogy. Think of a monthly stocktake rather than a one-off count. The count itself is mildly interesting. What changed since last month is the finding.

A new name nobody mentioned, a service that quietly moved to a different data centre, a name plate that disappeared — all of those show up in the difference, not in the snapshot.

Everything in Part D, in one file. Read-only, no dependencies beyond dig, and safe to run against your own estate on a schedule.

bash
#!/usr/bin/env bash
# dns-audit.sh <domain> [wordlist]
# Read-only DNS inventory. Run only against domains you own or are
# authorised to assess.
set -uo pipefail
DOMAIN="${1:?usage: dns-audit.sh <domain> [wordlist]}"
WORDLIST="${2:-}"

hr() { printf '\n== %s ==\n' "$1"; }

hr "ZONE RECORDS: $DOMAIN"
for t in SOA NS A AAAA MX TXT CAA DNSKEY; do
  out=$(dig +noall +answer "$DOMAIN" "$t")
  [ -n "$out" ] && echo "$out" || printf '%-8s  (none)\n' "$t"
done

hr "AUTHORITATIVE SERVERS"
dig +short "$DOMAIN" NS | sort

hr "WILDCARD CHECK"
probe="zz$RANDOM$RANDOM-wildcard-probe.$DOMAIN"
st=$(dig "$probe" +noall +comments | grep -o 'status: [A-Z]*' | head -1)
if [ "$st" = "status: NXDOMAIN" ]; then
  echo "no wildcard  ($st) -> existence checks are meaningful"
else
  echo "WILDCARD PRESENT ($st) -> every name resolves; enumeration unreliable"
fi

hr "ZONE TRANSFER (AXFR) ATTEMPT"
for ns in $(dig +short "$DOMAIN" NS); do
  if dig "@$ns" "$DOMAIN" AXFR +time=3 +tries=1 2>/dev/null | grep -q "^$DOMAIN"; then
    echo "OPEN AXFR on $ns  <-- FINDING, report it"
  else
    echo "refused/timeout on $ns  (expected)"
  fi
done

[ -z "$WORDLIST" ] && { hr "DONE"; exit 0; }

hr "NAME -> ADDRESS INVENTORY"
printf '%-34s %-8s %s\n' "NAME" "STATUS" "RESOLVES TO"
while read -r w; do
  [ -z "$w" ] && continue
  n="$w.$DOMAIN"
  ans=$(dig +noall +answer "$n" A)
  [ -z "$ans" ] && continue
  ips=$(echo "$ans" | awk '$4=="A"{print $5}' | paste -sd, -)
  cn=$(echo "$ans"  | awk '$4=="CNAME"{print $5}' | head -1)
  if [ -n "$cn" ]; then
    printf '%-34s %-8s CNAME %s -> %s\n' "$n" "ALIAS" "$cn" "${ips:--}"
  else
    printf '%-34s %-8s %s\n' "$n" "A" "$ips"
  fi
done < "$WORDLIST"

hr "REVERSE DNS OF DISCOVERED ADDRESSES"
while read -r w; do
  [ -z "$w" ] && continue
  dig +short "$w.$DOMAIN" A
done < "$WORDLIST" | grep -E '^[0-9]+\.' | sort -u | while read -r ip; do
  printf '%-16s %s\n' "$ip" "$(dig +short -x "$ip" | head -1)"
done
🧪 Exercise D5.1 — Run the whole audit end to end
bash
chmod +x dns-audit.sh
printf '%s\n' www mail smtp dev demo jira confluence support crm gw intranet sds api admin vpn > words.txt
./dns-audit.sh seamless.se words.txt
Expected result — click to reveal
plain text
== WILDCARD CHECK ==
no wildcard  (status: NXDOMAIN) -> existence checks are meaningful

== ZONE TRANSFER (AXFR) ATTEMPT ==
refused/timeout on ns-1490.awsdns-58.org.  (expected)
refused/timeout on ns-722.awsdns-26.net.  (expected)
refused/timeout on ns-135.awsdns-16.com.  (expected)
refused/timeout on ns-1760.awsdns-28.co.uk.  (expected)

== NAME -> ADDRESS INVENTORY ==
NAME                               STATUS   RESOLVES TO
www.seamless.se                    ALIAS    CNAME seamless.se. -> 52.77.52.233
mail.seamless.se                   ALIAS    CNAME ghs.googlehosted.com. -> 192.178.209.121
smtp.seamless.se                   ALIAS    CNAME aspmx.l.google.com. -> 192.178.209.27
dev.seamless.se                    A        52.76.82.127
demo.seamless.se                   A        195.22.81.226
jira.seamless.se                   A        14.99.30.4
confluence.seamless.se             A        14.99.30.4
support.seamless.se                A        14.99.30.4
crm.seamless.se                    A        14.142.42.18
gw.seamless.se                     A        195.22.81.194
intranet.seamless.se               ALIAS    CNAME ghs.google.com. -> 142.251.183.121
sds.seamless.se                    ALIAS    CNAME seamless.se. -> 52.77.52.233

== REVERSE DNS OF DISCOVERED ADDRESSES ==
14.142.42.18     
14.99.30.4       
195.22.81.194    gw.seamless.se.
195.22.81.226    
52.76.82.127     ec2-52-76-82-127.ap-southeast-1.compute.amazonaws.com.
52.77.52.233     ec2-52-77-52-233.ap-southeast-1.compute.amazonaws.com.

The Google addresses will differ every run — those CNAMEs resolve into a large rotating pool, which is itself worth noticing.

Four design decisions in this script are worth copying into anything you write:

1. The wildcard check runs before the enumeration and is printed prominently. If a wildcard exists, everything below it is noise, and the reader needs to know that before they read it.

2. The AXFR loop covers every nameserver. As D3 explained, the open one is usually the forgotten secondary, so checking only the first is checking the wrong one.

3. CNAMEs are labelled ALIAS and the target is kept. A flattened +short output would show only the final address and hide the fact that a third party sits in the path. Knowing that mail points into Google's estate is the useful part.

4. The reverse lookup is a separate final section. It is the part that tells you about hosting, region and mail reputation, and it is the part people leave out.

Where to take it next. Emit CSV and commit it to Git, and every run becomes a diff. A new name appearing, an address moving between hosting environments, or a PTR disappearing all become visible in a code review rather than in an incident. That is the point of the whole exercise: not the snapshot, but noticing the change.


D6 · What these names are called, and how deep they go

The analogy. Think of rooms in your house versus a floor you rent out.

Adding a room is still your house — you can repaint it this afternoon without asking anyone. That is a subdomain: just another record in your own zone.

Renting a floor to another company means they control it now. Same building, different keys, and any change goes through them. That is a delegated zone — and one command tells you which of the two you are looking at.

Module 01 A2 established that a name is a path of labels. Here is the vocabulary that goes with it, because the words get used loosely and the looseness is where the confusion starts.

NameWhat it is
se.The TLD. One label under the root
seamless.se.The registrable domain, and here also the zone apex — the name the zone is named after
admin-vfo.seamless.se.A subdomain of seamless.se. Third level. Also just "a domain name" — there is no separate word for it
www.admin-vfo.seamless.se.A subdomain of admin-vfo.seamless.se, which is itself a subdomain of seamless.se. Fourth level
"Subdomain" is a relative word, not a rank. seamless.se is a subdomain of se, exactly as admin-vfo.seamless.se is a subdomain of seamless.se. Nothing in DNS marks one level as special — people call the third level "the subdomain" purely out of habit, because that is where most organisations put their hosts.

So there is no special name for admin-vfo.seamless.se. It is a domain name, and it is a subdomain of seamless.se. If you need to be precise about depth, say third-level or fourth-level.

The distinction that actually matters, and it is the one nearly everybody misses: a subdomain is not the same thing as a zone.

Adding admin-vfo.seamless.se to your DNS does not create a new zone. It creates a record inside the existing seamless.se zone — same file, same nameservers, same SOA, same administrator.

A zone is only created when the parent formally hands authority over with NS records — a delegation. That is a deliberate act, usually because a different team or company will administer that branch.

And you can tell which you are looking at with one query, which is the exercise below.

🧪 Exercise D6.1 — Is this a subdomain, or its own zone?
bash
# a name that exists three levels deep
dig +short admin-vfo.seamless.se A

# THE test: ask which zone is authoritative for it.
# Any query that returns an SOA tells you which zone owns the name.
dig +noall +authority admin-vfo.seamless.se AAAA

# and check directly for a delegation
dig +noall +answer admin-vfo.seamless.se NS
Expected result — click to reveal
plain text
$ dig +short admin-vfo.seamless.se A
185.64.27.150

$ dig +noall +authority admin-vfo.seamless.se AAAA
seamless.se.		900	IN	SOA	ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

$ dig +noall +answer admin-vfo.seamless.se NS
(no output)

The second command is the whole exercise, and the answer is in the owner-name column. The SOA that came back belongs to seamless.se, not to admin-vfo.seamless.se.

That single fact tells you admin-vfo.seamless.se is a plain record inside the seamless.se zone. No separate zone, no separate nameservers, no separate administrator. It is a line in the same file as www and jira, and it is changed in the same Route 53 hosted zone.

The third command confirms it from the other direction — no NS records at that name, so nothing has been delegated.

What it would look like if it were a zone. The SOA in the AUTHORITY section would read admin-vfo.seamless.se. rather than seamless.se., and dig NS admin-vfo.seamless.se would return nameservers. Then it would be a genuinely separate administrative unit — possibly run by another team, possibly by the customer themselves.

Why you care in daily work. It answers "who do I ask to change this record". A subdomain-that-is-a-record is yours to change in your own hosted zone. A subdomain-that-is-a-zone belongs to whoever holds those nameservers, and your change has to go through them. Getting this backwards costs a day of chasing the wrong team. Module 03 covers how the delegation itself works.

Now the thing that catches people with www. prefixes — and it is worth checking before you assume a URL works.

admin-vfo.seamless.se exists. www.admin-vfo.seamless.se returns NXDOMAIN — it does not exist at all.

This is Module 01 A2.1 restated one level deeper: nothing in DNS makes www.X follow from X. Somebody has to create it, one name at a time. www.seamless.se exists only because a CNAME was deliberately added for it; nobody added the equivalent for admin-vfo, so that name simply is not there.

The practical habit: when a URL fails, dig the exact hostname from the address bar — including the www. — before you look at anything else. Half the time the hostname was never created.

🧪 Exercise D6.2 — The gap in the D5 script: it only looks one level deep
bash
# D3 and D5 probe <word>.seamless.se - a single label.
# Deeper names need a nested sweep.
L1="admin-vfo admin-vodafone dev demo jira"
L2="www admin api portal test mail vpn"

printf '%-40s %-10s %s\n' "NAME" "STATUS" "ADDRESS"
for a in $L1; do
  st=$(dig "$a.seamless.se" +noall +comments | grep -o 'status: [A-Z]*' | head -1 | cut -d' ' -f2)
  printf '%-40s %-10s %s\n' "$a.seamless.se" "$st" "$(dig +short $a.seamless.se A | tr '\n' ' ')"
  for b in $L2; do
    st2=$(dig "$b.$a.seamless.se" +noall +comments | grep -o 'status: [A-Z]*' | head -1 | cut -d' ' -f2)
    [ "$st2" = "NOERROR" ] && printf '  %-38s %-10s %s\n' "$b.$a.seamless.se" "$st2" "$(dig +short $b.$a.seamless.se A | tr '\n' ' ')"
  done
done
Expected result — click to reveal
plain text
NAME                                     STATUS     ADDRESS
admin-vfo.seamless.se                    NOERROR    185.64.27.150
admin-vodafone.seamless.se               NOERROR    77.83.61.131
dev.seamless.se                          NOERROR    52.76.82.127
demo.seamless.se                         NOERROR    195.22.81.226
jira.seamless.se                         NOERROR    14.99.30.4

Thirty-five fourth-level probes and not one of them exists. The indented lines never printed, because every www.*, admin.* and api.* under those five names returned NXDOMAIN.

That is a real and useful finding: this estate is flat. Everything lives one label under seamless.se. There is no www.admin-vfo, no api.dev, no nested structure at all. Knowing the shape is flat means the one-level sweep in D5 is sufficient here, and you can stop looking.

Also note admin-vfo and admin-vodafone are different names on different addresses185.64.27.150 and 77.83.61.131. Similar naming, separate environments. Do not assume two names that look related point anywhere near each other; the address column is the only thing that tells you.

Why brute force gets expensive fast, and why you should not lead with it. One level with a 5,000-word list is 5,000 queries. Two levels is 5,000 × 5,000 — twenty-five million, which is neither polite nor practical. So the realistic method is: sweep level one, then sweep level two only beneath the names that actually exist, which is what the nested loop above does.

Which is exactly why Certificate Transparency (D3.2) matters more at depth than at level one. CT gives you the full hostname of anything that ever had a public certificate, however deep it sits, with no guessing and no query volume. For a nested estate, run CT first and use the dictionary only to fill gaps.

And the reminder from D2: all of this depends on there being no wildcard. Note too that a wildcard at *.seamless.se would not match www.admin-vfo.seamless.se — wildcards cover one label only, so a fourth-level name is unaffected by a third-level wildcard. That asymmetry surprises people during audits.

🎯 Interview questions — Subdomains and zones

Q. What is the difference between a subdomain and a zone?

A subdomain is a naming relationship — any name below another in the tree. A zone is an administrative unit: a set of records served together by one set of nameservers, with one SOA.

Creating admin.example.com normally just adds a record to the example.com zone. It becomes a separate zone only when the parent publishes NS records for it — a delegation — which is a deliberate handover of authority, usually to a different team or organisation.

The one-command test, which is what makes this a practical answer rather than a definition: query any name under it and read the owner name of the SOA in the AUTHORITY section. If the SOA says example.com, the name is a record in the parent zone. If it says admin.example.com, it is its own zone. That tells you immediately whether the change is yours to make or somebody else's.

Q. example.com works but www.example.com does not. What happened?

They are two different names in the tree and nothing in DNS links them. www has to be created explicitly, usually as a CNAME to the apex or as its own address record, and in this case nobody did — or it was deleted.

I would confirm with dig www.example.com +noall +comments and expect NXDOMAIN, then check the authoritative server directly to rule out a cached negative.

The reason this is worth getting right instantly: the www convention is so universal that people assume it is a protocol behaviour. It is not — it is a record someone has to maintain, and "we migrated DNS and forgot the www CNAME" is a genuinely common outage. The same reasoning applies at any depth: www.admin.example.com does not follow from admin.example.com either.


Part E · Putting it together

E1 · How this all fits — the complete picture

Diagram source
flowchart TD
    subgraph ZONE["📗 THE ZONE - one file, one administrative unit"]
        SOA["SOA - exactly one, at the apex<br>serial, timers, negative TTL"]
        NS["NS - who is authoritative"]
        APEX["apex records<br>A, MX, TXT/SPF, CAA<br>NO CNAME allowed here"]
        SUB["names below the apex<br>A · AAAA · CNAME · SRV · TXT"]
        WILD["* wildcard<br>fires only when a name<br>does not otherwise exist"]
    end
    ZONE --> RRSET["every name+type is an RRSET<br>shared TTL · unordered<br>replaced as a whole"]
    RRSET --> Q["a query names ONE name<br>and ONE type"]
    Q --> R{"what comes back"}
    R -->|"records"| OK["NOERROR + data"]
    R -->|"name exists,<br>wrong type"| ND["NOERROR + ANSWER 0"]
    R -->|"no such name"| NX["NXDOMAIN<br>SOA carries the negative TTL"]
    ENUM["there is NO list operation<br>-> provider API · AXFR · CT logs<br>dictionary · NSEC · your own records"]
    Q -.->|"which is why"| ENUM
    style SOA fill:#8b5cf6,color:#fff
    style WILD fill:#ef4444,color:#fff
    style RRSET fill:#f59e0b,color:#fff
    style ENUM fill:#22c55e,color:#fff

Four ideas from this module, and everything else follows from them.

  1. The RRset is the unit. Shared TTL, no order, replaced whole. This explains DNS APIs, DNSSEC signing, and the automation bug that deletes five records while adding one.
  2. A CNAME captures the whole name, not one record type. Every CNAME rule — no coexisting records, illegal at the apex, breaks mail — is that one sentence applied.
  3. OK from named-checkzone means "this parses", not "this is correct". The expensive mistakes all load cleanly.
  4. There is no list operation. Enumeration is always reconstruction, its completeness depends on the source, and a wildcard destroys it entirely.

E2 · Production practice

HabitWhy
Treat every DNS API change as a whole-RRset replacementSending one A record to an API that takes the set deletes the other five, returns success, and looks like it worked
Run named-checkzone -D and read the dump, not just the OKThe trailing-dot bug, @ expansion and inherited TTLs are only visible in the canonical form
Never write a half-qualified name in a zone filewww or www.example.com. — the middle form silently becomes www.example.com.example.com.
Check for a wildcard before trusting any existence resultOne query decides whether every NXDOMAIN in your investigation means anything at all
Verify record changes against an authoritative server, not a resolveraa set and a full TTL is ground truth; anything else may be a cache showing you the past
Follow MX and CNAME targets one hop furtherAn MX pointing at a hostname with no address, or at a CNAME, is a silent partial mail outage
Publish a null MX on every domain that receives no mailOtherwise senders fall back to the A record and retry for days, generating tickets
Publish CAA naming every CA you actually use, plus iodefWith no CAA, every trusted CA on earth may issue for you. With too narrow a CAA, renewal fails at 3am
Read pct= whenever you read p= in a DMARC recordp=reject; pct=25 is a rollout in progress that reads as "protected" to anyone skimming
Count your SPF include: mechanisms against the 10-lookup limitA vendor expanding their record pushes you to permerror and bounces your mail with no change on your side
Test AXFR against every nameserver, not just the firstThe open one is nearly always a forgotten secondary, not the primary
Sort your inventory by address, not by nameCo-location and single points of failure are invisible in a name list and obvious in an address list
Commit the audit output to Git and review the diffThe snapshot is mildly interesting; the change between snapshots is the finding

E3 · Capstone exercise

Two halves: build a zone correctly, then read a zone you do not control. Do both without scrolling back.

Part 1 — Build. Write a zone file for corp.internal that satisfies all of the following, and prove each with named-checkzone:

  1. Apex serves a web address, and www reaches the same place — without breaking mail
  2. api is served by three addresses that must fail over together
  3. Mail is handled by two servers with a clear primary and fallback, neither target an alias
  4. A subdomain that accepts no mail at all is declared as such, machine-readably
  5. Only one named CA may issue certificates, and violations are reported somewhere
  6. A negative answer for this zone may be cached for no more than 5 minutes
  7. The zone must load with zero warnings under named-checkzone

Part 2 — Read. For any domain you are authorised to assess, produce a one-page report answering:

  1. Who operates the DNS, who operates mail, and how you know
  2. Whether the zone has a wildcard, and what that means for the rest of your report
  3. Every name you can find, with its serving address, sorted by address
  4. The single largest concentration of risk visible in the address list
  5. Three findings that are about what is missing rather than what is present
Model answer — attempt it first, then click

Part 1 — the zone.

plain text
$TTL 3600
$ORIGIN corp.internal.
@       IN  SOA ns1.corp.internal. hostmaster.corp.internal. (
                2026081701  ; serial
                7200        ; refresh
                900         ; retry
                1209600     ; expire
                300 )       ; requirement 6 - negative TTL 5 min
@       IN  NS  ns1.corp.internal.
@       IN  NS  ns2.corp.internal.
ns1     IN  A   192.0.2.10
ns2     IN  A   192.0.2.11

; requirement 1 - apex CANNOT be a CNAME, so a real A record,
; and www is an alias to the apex. The alias is at www, not at @.
@       IN  A   192.0.2.20
www     IN  CNAME @

; requirement 2 - one RRset, three members, one shared TTL
api     IN  A   192.0.2.30
api     IN  A   192.0.2.31
api     IN  A   192.0.2.32

; requirement 3 - targets are real hostnames with A records, not aliases
@       IN  MX  10 mail1.corp.internal.
@       IN  MX  20 mail2.corp.internal.
mail1   IN  A   192.0.2.40
mail2   IN  A   192.0.2.41

; requirement 4 - null MX, RFC 7505
static  IN  A   192.0.2.50
static  IN  MX  0 .

; requirement 5 - single issuer plus violation reporting
@       IN  CAA 0 issue "letsencrypt.org"
@       IN  CAA 0 iodef "mailto:[email protected]"
bash
named-checkzone corp.internal db.corp.internal      # requirement 7
named-checkzone -D corp.internal db.corp.internal   # and READ it

The seven traps, and where each requirement tries to catch you:

  1. Requirement 1 is the apex-CNAME trap. A CNAME at @ collides with SOA, NS, MX and CAACNAME and other data, zone rejected. The alias belongs at www
  2. Requirement 2's phrase "fail over together" is the RRset property: one TTL, shared, not chosen per record. Writing three different TTLs gets you TTL set to prior TTL and two silently discarded values
  3. Requirement 3 says "neither target an alias" — an MX pointing at a CNAME is illegal per RFC 2181 and named-checkzone warns while still returning OK, which is the trap
  4. Requirement 4 is MX 0 . — a single MX whose target is the root. A missing MX is not the same thing: senders then fall back to the A record
  5. Requirement 5 needs two records, not one. issue alone silently discards violation reports; iodef is what makes them reach you
  6. Requirement 6 is the SOA MINIMUM field, and the subtlety is that the effective negative TTL is min(MINIMUM, SOA TTL). With $TTL 3600 and MINIMUM 300, you get 300 — correct. Had you set MINIMUM to 7200 you would have got 3600, not 7200
  7. Requirement 7 means reading the output. OK alone does not satisfy it, because OK appears alongside warnings

Part 2 — the report, using seamless.se as the worked example.

8. Operators. DNS is AWS Route 53 — the four awsdns nameservers, spread across .com, .net, .org and .co.uk so no single TLD is a shared failure. Mail is Google Workspaceaspmx.l.google.com at preference 10 with a 20/30 fallback ladder. Both conclusions come from record values, not from asking anyone.

9. Wildcard. None. A random name returns NXDOMAIN, so every negative result in this report is real information rather than an artefact. This has to be stated first, because without it nothing below can be relied on.

10 and 11. Inventory sorted by address:

plain text
14.99.30.4       jira · confluence · support     <-- concentration
14.142.42.18     crm
52.76.82.127     dev                             AWS ap-southeast-1
52.77.52.233     www · sds  (apex)               AWS ap-southeast-1
195.22.81.194    gw                              PTR agrees - FCrDNS ok
195.22.81.226    demo

The largest concentration of risk is 14.99.30.4: Jira, Confluence and the support system on one address. One host, one certificate, one firewall rule, one reboot. It is invisible in a list of names and unmissable once sorted by address — which is the entire reason to sort by address.

12. Three findings about absences — the ones a "what exists" report never produces:

  • No AAAA anywhere. IPv6-only clients cannot reach this estate without translation, and IPv6-only mobile networks are increasingly common
  • No CAA record. Every publicly trusted CA in the world is currently authorised to issue certificates for this domain. One record fixes it
  • No PTR for 14.99.30.4, 14.142.42.18 or 195.22.81.226 — and the SPF record authorises sending from those very ranges. Any mail from them is exposed to a reverse-DNS deliverability penalty

The five things most people miss on this capstone:

  1. Putting the wildcard check first in Part 2. It is a precondition for the report's validity, not a bullet point inside it
  2. iodef as well as issue. Almost everyone writes the issue record alone
  3. Reporting absences at all. Requirement 12 exists because "what is missing" is where the security findings live, and a tool-generated inventory never surfaces them
  4. Sorting by address. The concentration finding is unreachable any other way
  5. Not confusing shared address with shared host. Behind a CDN or an ingress, co-location is an artefact — the finding above holds only because the reverse DNS is empty and the range is not a CDN's, and saying so is what makes it a finding rather than a guess

E4 · Official documentation

LinkCovers
RFC 1035 §3.2–3.5 and §5Every original record type, and the master file format from Part C
RFC 2181 — Clarifications to the DNS Specification§5 RRsets and the shared-TTL rule · §10.1 CNAME restrictions · §10.3 MX and NS targets. The most operationally useful RFC in DNS
RFC 1912 — Common DNS Operational and Configuration ErrorsEffectively a checklist of every mistake in Part C. Short, and worth reading end to end
RFC 2308 — Negative CachingThe SOA MINIMUM field, and why the negative TTL is the lesser of two numbers
RFC 4592 — The Role of WildcardsAll four wildcard properties from C4, stated formally
RFC 3596 — AAAA · RFC 2782 — SRV · RFC 8659 — CAAThe three modern types, one RFC each
RFC 7208 — SPF · RFC 6376 — DKIM · RFC 7489 — DMARC · RFC 7505 — null MXEverything mail-related that lives in DNS
RFC 8482 — minimal responses to ANY · RFC 5936 — AXFRWhy ANY does not list a zone, and what a real zone transfer is
BIND 9 — Configurations and Zone Files · named-checkzoneThe zone file format as implemented, and the validator from C2
Route 53 — Supported DNS record types · alias vs non-aliasThe apex-CNAME workaround from C3, on the provider this module's examples use
IANA DNS ParametersThe live registry of every record type. Always current, unlike any RFC
How to read these efficiently.

Read RFC 1912 first and read all of it. It is short, it is entirely operational, and it is the only RFC here that reads like a checklist written by someone who has been paged.

Use RFC 2181 as a reference, not a read. Go to §5, §10.1 and §10.3 when you need to win an argument about RRset TTLs, CNAME coexistence or MX targets. Those three sections settle most real disputes.

For record types, go to IANA before any RFC. New types are still being added; the registry is current and links to the defining RFC for each.

The offline route. man named-checkzone and man dig cover the tooling. named-checkzone -D is itself documentation: it shows you what the server believes your file says, which is more useful than any specification when you are debugging one specific zone.


E5 · Self-assessment

Answer each out loud before opening it.

1. What is an RRset, and name two behaviours that follow from it.

Every record sharing an owner name, class and type — treated as one indivisible object.

Two consequences: all members must share a TTL, because they are cached and expire as one; and you replace the whole set, never one member, which is why DNS APIs are replace-oriented and why a script that sends a single value deletes the rest.

A third, if you want it: DNSSEC signs RRsets, not records.

2. Explain in one sentence why a CNAME cannot coexist with any other record.

Because a CNAME redirects the name, not a record type — once the name is an alias it is a signpost rather than a destination, so there is nowhere for other records to live.

Everything else follows: it cannot sit at the apex, because the apex must hold SOA and NS; and adding one to a name with MX records silently destroys mail delivery.

3. Name the seven SOA fields, and say which one you would change to make deleted records disappear faster.

MNAME, RNAME, SERIAL, REFRESH, RETRY, EXPIRE, MINIMUM.

MINIMUM — it is the negative caching TTL, so it governs how long "this does not exist" is remembered.

The precision that matters: the effective negative TTL is the lesser of MINIMUM and the SOA record's own TTL, so lowering MINIMUM alone does nothing if the SOA's TTL is already the smaller number.

4. Why does dig ANY example.com not give you every record?

Because ANY never meant that. It returns whatever the answering server happens to hold — cached fragments from a resolver, and never a guaranteed inventory from an authoritative server.

Since RFC 8482 most large operators return a minimal synthetic response instead, often a HINFO record reading RFC8482, because ANY was the engine of DNS amplification attacks.

The real answer is that DNS has no listing operation at all, so you loop over types.

5. What are the four properties of a wildcard record?

It fires only when the queried name does not otherwise exist in any form; it matches exactly one label; it is type-specific; and the response carries the queried name, so nothing reveals that a wildcard produced it.

The consequence that matters operationally: a wildcard abolishes NXDOMAIN for the whole zone, which destroys existence checks, monitoring that relies on a name failing, and every enumeration technique.

6. named-checkzone prints OK. What have you actually proved?

That the file parses. Nothing more.

The trailing-dot bug, an MX pointing at a CNAME, and mismatched TTLs in an RRset all produce OK — two with a warning, one with complete silence.

The verification that counts is named-checkzone -D — read the canonical dump — and then querying the loaded zone.

7. Why can't you put a CNAME at the apex, and what does your provider offer instead?

The apex must carry SOA and NS, and a CNAME excludes all other types at the same name. It is a structural contradiction.

Providers offer alias-style pseudo-records — Route 53 alias, Cloudflare CNAME flattening, ALIAS / ANAME elsewhere. The authoritative server resolves the target internally and emits a real A or AAAA, so nothing non-standard goes on the wire.

Two catches: the flattening happens from the provider's network position, which can hand your users a badly chosen address for a geo-distributed target; and the records are provider-specific, so a DNS migration means re-creating each one by hand.

8. Someone asks you to list every subdomain of a domain. What is the first thing you say?

That DNS has no listing operation, so this is reconstruction from indirect sources and its completeness depends entirely on which sources I can reach.

If it is our own domain, reading the zone from the provider's API is the only complete method and ends the exercise.

Otherwise: AXFR against every nameserver, Certificate Transparency logs, dictionary probing, NSEC walking if the zone is signed, and mining our own SPF, MX, NS and reverse DNS for infrastructure.

And before any of that: authorisation in writing, and a wildcard check — with a wildcard present, every result is meaningless.

9. dig +short name.example.com prints nothing. List everything that could mean.

NXDOMAIN — no such name. NODATA — the name exists but has no A record, perhaps only a CNAME, TXT or AAAA. SERVFAIL — the resolver could not complete. REFUSED — policy. Or no response at all — a timeout.

+short collapses all five into one empty line, which is why it must never appear in a check.

The correct procedure is +noall +comments to read status:, then other record types, then a wildcard probe, then confirmation against an authoritative server.

10. You find three business services on one IP address. Is that a finding?

Only after one check. Behind a CDN, reverse proxy, Kubernetes ingress or cloud load balancer, hundreds of unrelated names legitimately share an address and the co-location is an artefact.

I would check the reverse DNS and the address's owner. If the PTR is a CDN's, it is noise. If the PTR is empty and the range belongs to the organisation, it is a genuine single point of failure worth raising.

Stating that qualification is what turns a guess into a finding — and it is the difference between a report people act on and one they stop reading.

11. A domain has p=reject in DMARC and no CAA record. Summarise its posture.

Mixed, and both halves need a caveat.

On mail: p=reject is the strongest DMARC policy, but I would read pct= alongside it — p=reject; pct=25 is a rollout enforcing on a quarter of failing mail, which reads as "protected" to anyone skimming.

On certificates: no CAA means every publicly trusted CA in the world may issue for this domain. It is not a blocked state, it is a wide-open default, and it is one of the cheapest findings to remediate — one record, no traffic impact.


Next — Module 03 · Delegation, Recursion & Caching.

You now know what an NS record is and that example.com is delegated by com. Module 03 follows a resolver as it walks that path for real: root hints, referrals, glue records and why they must exist, dig +trace line by line, how the cache is populated at every step, and what "DNS propagation" is actually describing. It also explains the AUTHORITY section you saw filled with 13 records in Module 01 C4.1 and never had explained.

📚 Sources for the interview questions

All record data in this module was captured live against seamless.se, example.com, google.com and jabber.org on 17 August 2026 using dig 9.18, and every zone-file error in Part C was reproduced with named-checkzone 9.18. Addresses and TTLs will have moved on by the time you read it; the shapes will not.

Specifications verified directly: RFC 1035, RFC 1912, RFC 2181, RFC 2308, RFC 2782, RFC 3596, RFC 4592, RFC 5936, RFC 7208, RFC 7489, RFC 7505, RFC 8482, RFC 8659, RFC 9162, and the IANA DNS Parameters registry.

Question selection cross-referenced against publicly published 2026 DNS and networking interview question sets:

Answers were rewritten and deepened rather than reproduced. The published sets ask "what is an MX record" and answer in one line; the operational half — the 10-lookup SPF limit, pct= in DMARC, MX targets that must not be aliases, the null MX — is what actually gets discussed in the room.

Spotted a mistake or want something added? Send me a note.