docs/adr/0042-provider-dns-private-recursor.md
Rama master · réplica de lectura del repositorio original.
# ADR-0042: Private Recursor for the DNS Consensus Secondary
## 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-07 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0004, ADR-0039, ADR-0040 |
| **Project** | DOMINUS |
## Context
The 2-of-3 DNS consensus gate (ADR-0039, ADR-0040) requires the secondary
provider to use a resolver strategy disjoint from the primary
(`DNS_CONSENSUS_STRATEGY`, default `dot-only`). On the Cloud topology
(ADR-0026, ADR-0027) the app runs on a single VM, and `dot-only` relies on
egress TCP/853 to public DoT resolvers discovered via DHCP.
That egress is **not guaranteed** on a single-VM deployment (C3 of the
cloud hardening review): the hosting provider may filter outbound TCP/853,
or the DHCP-supplied resolver list may be empty/unusable. When that
happens, every consensus secondary query fails, every Available verdict is
downgraded to Unknown, and the whole consensus gate degrades (ADR-0039)
even though the primary DoH/DoT path works fine. The secondary is the
consensus *chokepoint*: its strategy must be viable in the environment
where the app actually runs.
Public DoT via the same endpoints as the primary was rejected because the
consensus gate exists precisely to avoid sharing resolver endpoints
(ADR-0039 disjointness check).
## Decision
**Pin the consensus secondary to a private, co-hosted recursive resolver
(Unbound) via an optional `DNS_CONSENSUS_NAMESERVERS` setting.**
- `DNS_CONSENSUS_NAMESERVERS` (comma-separated `host` or `host:port`
addresses) is an optional setting. When set, the secondary provider is
built with `lookupStrategy: 'native'` and `nameservers` pointing at those
addresses — overriding `DNS_CONSENSUS_STRATEGY` verbatim.
- When unset, behaviour is unchanged: `DNS_CONSENSUS_STRATEGY` is used as
before (backward compatible).
- The recursor is an optional compose override
(`docker-compose.dns-consensus.yml`): a `mvance/unbound` service (the
community-maintained image recommended by the Unbound project — NLnet Labs
publishes no official image, and the original `nlnetlabs/unbound` reference
did not exist on Docker Hub) on a dedicated `/24` subnet (`172.20.0.0/24`),
pinned to the deterministic address `172.20.0.10:5300`, with `api`,
`worker`, and `scheduler` joining that network. The image is digest-pinned
and amd64-only (ADR-0046). The base and prod compose files are untouched;
the override is opt-in via `-f docker-compose.dns-consensus.yml`.
- Node's `resolver.setServers()` only accepts IP literals, not service
hostnames — hence the fixed IPAM address instead of a service name.
- Unbound config (`deploy/unbound/unbound.conf`): recursive resolution from
the root (no forwarding), qname-minimisation, DNSSEC validation enabled,
ACL restricted to loopback + the compose `/24`, localhost remote-control
for the healthcheck.
### Options considered and rejected
- **Keep `dot-only` and add retries/fallback** — rejected: the failure is
environmental (egress TCP/853 filtered or no DHCP resolver), not
transient; retries cannot fix a blocked port.
- **Use the primary's DoH endpoints for the secondary** — rejected:
violates the endpoint-disjointness check that is the entire point of the
gate (ADR-0039).
- **Run the recursor inside the base prod compose** — rejected: forces the
recursor on every deployment; it is only needed when public DoT egress is
unavailable. An opt-in override keeps the base topology stable (CI
topology check unchanged).
- **Forward to a fixed public recursor (1.1.1.1, 8.8.8.8) via plain UDP** —
rejected: the second opinion must not come from the same resolvers the
primary can reach via DoH; a co-hosted recursor keeps the opinions
genuinely disjoint.
## Consequences
### Positive
- The consensus gate keeps its independent second opinion even when the VM
cannot reach public DoT resolvers; egress TCP/853 is no longer a
deployment requirement.
- Fully backward compatible: unset `DNS_CONSENSUS_NAMESERVERS` preserves
the ADR-0039/0040 behaviour and all existing tests.
- The compose override is opt-in and does not perturb the validated base
topology or the CI compose checks.
### Negative
- The recursor is another co-hosted service with its own CPU/memory budget
(bounded via `deploy.resources`).
- A compromised recursor could influence the secondary opinion; mitigated
by the loopback/subnet ACL and by DNSSEC validation in Unbound.
### Risks and mitigation
- **Misconfiguration** (pin reusing the primary resolvers, e.g.
`DNS_CONSENSUS_NAMESERVERS=1.1.1.1` while the primary DoH uses 1.1.1.1) —
the existing endpoint-disjointness check treats reused endpoints as an
invalid setup and refuses to enable consensus, same as ADR-0039.
- **Unbound not reachable** (override not deployed) — the native secondary
fails like any other secondary failure and the ADR-0039 degradation
policy applies; the operator sees the provider status note pointing at
the configured recursor.