A Resolver rule outranks your private hosted zone, and the zone is never consulted
Two DNS sources can claim the same domain in one VPC. The Resolver rule wins at equal specificity and the hosted zone is never read — and nothing in the answer reliably tells you which source served it, so the check has to be a configuration check.
· Venerable Networks
You can configure two things in one VPC that both claim to answer for corp.internal: a private hosted zone, and a Route 53 Resolver forwarding rule. Nothing warns you. Both are valid, both are associated, and one of them silently never gets consulted.
The evaluation order, per AWS troubleshooting guidance, is most-specific-match first. Where specificity is equal:
- Resolver rule
- Private hosted zone
- Public hosted zone
And stated directly elsewhere: if a Resolver forwarder rule and a private hosted zone conflict, the Resolver rule takes precedence.
The failure this produces
A migration moves a service into AWS. A private hosted zone is created for corp.internal with the new address, associated with the VPC, and verified in the console. Correct.
What nobody looks at is a forwarding rule for corp.internal created eighteen months earlier so the VPC could resolve data centre names. It is still associated. It is still doing its job.
So every lookup under corp.internal is forwarded to the corporate resolver, which still holds the pre-migration record. The application resolves the name, connects successfully, and talks to the host that was supposed to be decommissioned.
Nothing fails. There is no error, no elevated latency, no failed health check. The system is working correctly and doing the wrong thing, which is worse than an outage because the diagnostic instinct that serves you everywhere else — follow the failing request — has nothing to grab.
Why the obvious check passes
"Is the DNS record right?" is the correct first question, and the answer is yes. Verifying a record in the Route 53 console tells you what the zone contains. It tells you nothing about whether the zone is being consulted.
Both facts are simultaneously true: the zone holds the right answer, and the resolver returns a different one. That is the point where people start re-checking things they have already checked.
The answer itself will not tell you
The tempting move is to look for a tell in the response. There is a partial one — compare the TTL against your record:
dig +noall +answer payments.corp.internal
# payments.corp.internal. 0 IN A 10.100.2.87Your zone says 60 and this says 0, so this answer did not come from your zone. Useful. But it is a hint rather than a test, and it is worth being exact about why, because the asymmetry matters: a mismatched TTL is evidence, and a matching TTL is not evidence of anything.
The Resolver relays whatever the upstream sent. A TTL of 0 means the upstream chose 0 — which some resolvers do for locally-defined records, dnsmasq among them — not that forwarding is inherently marked. A corporate BIND, Windows DNS, or Infoblox server answering from its own zone will return its own TTL, and there is no reason that cannot also be 60. Then the TTL matches, the address is still wrong, and you have a check that just told you to look somewhere else.
So use the TTL as a prompt and settle it with the configuration, which is where precedence actually lives:
aws route53resolver list-resolver-rules \
--query 'ResolverRules[?RuleType==`FORWARD`].[DomainName,Name,Id]' --output tableAny FORWARD rule whose domain covers a name you also publish in a private hosted zone is this failure, present or latent, regardless of what today's answer looks like. That distinguishes "the zone is wrong" from "the zone is not being read" — completely different investigations — without depending on a stranger's TTL choices.
- EC2
Application in the VPC
payments.corp.internal
- RTB
Route 53 Resolver
equal specificity → rule before zone
- GW
Forwarding rule for corp.internal
answer relayed: 10.100.2.87, TTL 0
- VPCE
Private hosted zone corp.internal
holds payments → 10.100.1.44, TTL 60
Both sources claim corp.internal and both are associated with the VPC. The rule outranks the zone at equal specificity, so the rule answers and evaluation stops there — the greyed row is never consulted, which is why verifying the record in the console proves nothing. The two addresses differ, but so does the TTL, and the TTL is the field that tells you which of the two rows produced the answer.
After fixing it, the same check confirms the source changed rather than just the value:
dig +noall +answer payments.corp.internal
# payments.corp.internal. 60 IN A 10.100.1.44Address and TTL both moved. Check both.
The fix is specificity, not deletion
Deleting the forwarding rule is the tempting fix and it is wrong. The VPC genuinely needs data centre names, and removing the rule trades a silent misresolution for a loud outage across a set of services you probably cannot enumerate.
Make one claim more specific so specificity breaks the tie instead of precedence.
Narrow the forwarding rule. Leave corp.internal to the private hosted zone and forward only what actually lives on-premises — dc.corp.internal, for instance. This is the right shape when you control the namespace.
Or add a SYSTEM rule for the exception. A SYSTEM rule means "do not forward this". Being more specific, it beats the broad FORWARD rule:
resource "aws_route53_resolver_rule" "keep_local" {
name = "keep-payments-local"
domain_name = "payments.corp.internal"
rule_type = "SYSTEM"
}This scales badly — one rule per migrated name — but it is exactly right during a migration where names move one at a time.
Reverse DNS collides the other way
AWS auto-creates reverse lookup rules for your VPC CIDRs. A broad 10.in-addr.arpa forwarding rule aimed at a corporate resolver is less specific than those, so it loses.
That is the mirror image of the forward case and it surprises people in the opposite direction: they configure reverse forwarding, it appears to do nothing, and the reason is that the auto-created rules are more specific. Forwarding reverse zones means disabling the auto-created ones explicitly.
The design fix
The lasting answer is not a narrower rule; it is deciding that a namespace has exactly one authority. The convention that survives contact with reality is a dedicated subdomain per location — aws.corp.internal for private hosted zones, dc.corp.internal for forwarded names. Then no rule and no zone ever compete, and the precedence order becomes trivia you never need to know.
Instrument it
Route 53 Resolver query logging records the query, the response, and whether it was forwarded. With it enabled, this incident is one query away: every corp.internal lookup shows as forwarded and the answers do not match the hosted zone. Without it there is no record that DNS did anything at all, which is how six months pass.
And because this class of bug returns a valid answer, the alarm has to assert a value rather than detect an error:
test "$(dig +short payments.corp.internal)" = "$EXPECTED_IP" \
|| echo "FAIL: payments resolves to the wrong host""What would tell us if this were quietly wrong?" is a different question from "what would tell us if this broke", and only the second one usually gets built.
Two labs sit on either side of this. The hostname that existed in only one VPC is a zone that was never associated, where reaching for a Resolver endpoint would be an expensive way to paper over a free fix. The name that resolved to last year is a correctly associated zone being outranked by an endpoint that exists for a legitimate reason. Same two components, opposite failures.