Ctrl K
ComunidadGuías…

docs/adr/0042-provider-dns-private-recursor.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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.