docs/adr/0045-dns-consensus-tertiary-leg.md
Rama master · réplica de lectura del repositorio original.
# ADR-0045: DNS Consensus Third Leg
## Metadata
| Field | Value |
|-------|-------|
| **Status** | Superseded by [ADR-0072](0072-dns-consensus-revalidation-hardening.md) and [ADR-0075](0075-dns-unbound-single-source-completion.md) |
| **Date** | 2026-08-08 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0002, ADR-0039, ADR-0042, ADR-0044 |
| **Project** | DOMINUS |
## Context
The 2-of-3 DNS consensus gate (ADR-0039) demands a second, independent
resolver opinion before any `Available` verdict stands. Its failure policy is
fail-closed: when the secondary cannot answer, the domain is downgraded to
`Unknown` rather than trusted (ADR-0002 conservatism). That is the correct
bias for correctness — but it is also the gate's single point of failure:
- **The secondary's outage is the gate's outage.** ADR-0042 moved the
secondary to a co-hosted recursor (Unbound) precisely to insulate it from
public DoT egress, but a single leg still separates every `Available`
verdict from `Unknown` on *any* secondary failure mode: local recursor
crash, network partition to it, rate-limit exhaustion (ADR-0044), or a
transient bulk failure.
- **A dead secondary converts availability research into a false negative.**
With consensus enabled, a run over a live but silent secondary emits
`Unknown` for domains that are almost certainly available — the user passes
on acquisitions the tool would otherwise recommend. The gate is
conservative, but it is *wastefully* conservative when the failure is a
transient resolver problem rather than a genuine disagreement.
- **Consensus failures are already surfaced** via degraded-run flagging
(`DNS_CONSENSUS_DEGRADED_RATIO`, ADR-0039) and the `DnsConsensusStats`
verdict tallies, but nothing *acts* on them mid-run: degraded metrics tell
the operator after the fact what the gate could not distinguish — a
domain-level rescuing third opinion can.
What the existing design deliberately lacks is a **third, independent opinion
that may confirm what the secondary failed to confirm**. Two resolvers form
the minimum failing configuration; three end the single-point-of-failure on
the verification gate's own leg. The confidence cost is zero — a third
opinion confirms or vetoes; it can only rescue verdicts the gate would
otherwise drop.
## Decision
Add an optional **tertiary consensus leg** (ADR-0045), applied inside the
existing 2-of-3 consensus check on `Available` domains:
1. **Config, opt-in** — `DNS_TERTIARY_ENABLED` (default `false`); the leg is
off by default and participates only when explicitly enabled.
`DNS_TERTIARY_STRATEGY` (same strategy enum as the secondary, default
`'native'` — the system recursor) and `DNS_TERTIARY_NAMESERVERS`
(optional; when set, pins the leg to plain native DNS through those
addresses, mirroring ADR-0042's C3 pinning rule) select its resolver set.
2. **A third opinion only.** The tertiary is consulted **only when the
secondary cannot answer** (`requiredAvailable > secondary confirmations`).
When the secondary confirms, the tertiary is never queried — it adds no
cost to the happy path and no opinion when the gate already has one.
When the secondary says `Registered`, the veto is final and the tertiary
is never consulted.
3. **Rescue semantics.** A domain the tertiary resolves as `Available`, and
whose leg count still falls short of `DNS_CONSENSUS_REQUIRED_AVAILABLE`
(default `1`), is upgraded from `Unknown` to `Available` — `Verified`
with `tertiaryRescued` set. A `Registered` answer blocks the domain
(counts as disagreement → `Unknown`, never rescued). An unanswered leg
count of 2 leaves the domain at its conservative fallback — it now costs
*two* independent legs before an `Unknown` is manufactured.
4. **Strict independence, startup-checked.** The tertiary is built by
`BuildTertiaryConsensusProvider` in the composition root. Its resolver
endpoint set must be disjoint from *both* the primary and the secondary.
Overlapping configuration (the same recursors backing two legs) removes
the third opinion's independence, so the leg is dropped at startup with a
warning — the gate must never silently thin its redundancy by racing the
same endpoints twice. See ADR-0042 for why resolver-set overlap is treated
as disabled rather than *undesired*.
5. **Bounded effort.** The tertiary consults the same dedicated
consensus rate-limiter budget (ADR-0044), bounded by the same
`DNS_CONSENSUS_BULK_CONCURRENCY` ceiling; it can never outrun
or starve the secondary.
6. **Metrics.** `DnsConsensusMetrics` gains `tertiaryRescuedTotal`
(resolves rescued *by* the tertiary), emitted as
`dominus_dns_consensus_tertiary_rescued_total` (counter) in Prometheus,
and the run-level stats carry `tertiaryRescued` on the affected domain.
7. **`DNS_CONSENSUS_REQUIRED_AVAILABLE` (default `1`, range 1–2) tightens the
gate further.** With `1`, one confirmation — primary plus either the
secondary or the tertiary — stands as `Available`; with `2`, both the
secondary *and* the tertiary must confirm an `Available` verdict. When no
tertiary leg is configured, `2` degrades gracefully to `1` at run time
(with a one-time warning) rather than manufacturing `Unknown`s for the
whole run. This restores, in effect, ADR-0039's strict-2 confirmation for
operators who want it, while keeping the default cheap and independent.
## Consequences
### Positive
- **Single-point-of-failure removed from the confirmation leg.** A
transient secondary outage no longer manufactures a run of false `Unknown`s:
the tertiary rescues precisely the domains the gate intended to pass.
- **No happy-path cost.** The tertiary is only queried when the secondary
cannot confirm — zero added latency/cost (after the disjointness check) on
the confirmation path.
- **Veto stays absolute.** `Registered` from any consulted leg is final:
rescue only ever *restores* availability; it can never overturn a
registration verdict.
- **Operational observability** — `dominus_dns_consensus_tertiary_rescued_total`
names the failure mode directly: a rising rescue tally is a secondary-leg
outage in progress, visible without chasing logs.
### Negative
- **Three DNS opinions cost ~1 more query per rescued domain which the
gate would have dropped anyway.** The rate-limiter budget bounds it.
- **New config surface** (four environment variables) and a startup-time
disjointness check with no runtime re-dial: a misconfigured tertiary is
dropped once, at boot, not saned mid-run.
- **Rescue tallies are a separate counter, not mixed into verified.**
`verifiedTotal` keeps its strict meaning (secondary-confirmed only);
rescued verdicts live exclusively in `tertiaryRescuedTotal`, and the
run-level `tertiaryRescued` field is emitted only when > 0.
### Risks and mitigation
- **Rescued tallies can be mistaken for strict-two-opinion confirmations.**
With `DNS_CONSENSUS_REQUIRED_AVAILABLE=1` (default), a rescued domain was
confirmed by the primary + tertiary, not the primary + secondary. The
metric naming captures this (`tertiary_rescued_total`), and the default
stays `1`; operators who need strict two-opinion confirmation set it to
`2`, where a domain counts only with both secondary *and* tertiary.
- **Triple redundancy tanks cost.** The geometry that makes the leg useful
(secondary silent) is rare by definition; doubling the DNS budget for it
would violate the conservatism gate's cost discipline — mitigate: shared
budget, disabled by default, only consulted on failure.
### Related ADRs
- ADR-0002 (scoring conservatism — the gate under guard)
- ADR-0039 (2-of-3 consensus degradation policy)
- ADR-0042 (private recursor as the consensus secondary — C3 pinning reused)
- ADR-0044 (dedicated consensus budget — budget the tertiary must share)