docs/adr/0079-dns-fallback-fail-closed-hardening.md
Rama master · réplica de lectura del repositorio original.
# ADR-0079: DNS Fallback Fail-Closed Hardening — Config Honesty, Provenance Truth, Bulk Index Safety
## Metadata
| Field | Value |
| -------------- | -------------------------------------- |
| **Status** | Accepted |
| **Date** | 2026-10-01 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0076, ADR-0078, ADR-0073, ADR-0002 |
| **Project** | DOMINUS |
## Context
The DNS fallback path (ADR-0076) wraps `UnboundResolver` with a `FallbackResolver`
that delegates non-Available verdicts to `NodeDnsFallback` when the primary is
degraded. Three load-bearing defects were found during a resilience review:
1. **Silent DoH downgrade**: `createFallbackProvider('cloudflare-doh' | 'google-doh')`
logged a warning and returned `NodeDnsFallback`. An operator selecting DoH
redundancy unknowingly ran on the system resolver — a fail-open config lie.
2. **Provenance lie**: `UnboundResolver` stamped `dnssecSource: 'per-query'` for
every Available verdict whenever per-query validation was enabled, even when
the per-query call timed out, errored, or threw and the code fell back to the
resolver-level stamp. Audit trail claimed cryptographic proof that never happened.
3. **Bulk duplicate collapse**: `FallbackResolver.checkBulk()` merged fallback
results with `domains.findIndex((d) => d === domain)`. Duplicate domains in
one bulk request collapsed onto the first index, overwriting results and
dropping entries.
Per-query DNSSEC via `@relaycorp/dnssec` remains best-effort in production
(the default validate adapter throws and the resolver falls back to the
resolver-level negative-control proof). That behavior is retained; this ADR
only makes the provenance honest about which proof actually applied.
## Decision Drivers
1. **Fail-closed config (ADR-0002)** — Unimplemented options must throw, never silently downgrade.
2. **Provenance truth** — `dnssecSource` must reflect the proof actually used, not the proof configured.
3. **Bulk correctness** — Duplicate domains must resolve independently; index-based merge is the only safe strategy.
4. **Conservatism preserved** — No change to the Available-from-fallback safety gate; fallback still never serves Available.
## Considered Options
### Option A: Fail-closed + honest provenance + index merge (Chosen)
- `createFallbackProvider` throws for `cloudflare-doh` / `google-doh` with an actionable message.
- `UnboundResolver` tracks `perQueryValidated` and stamps `per-query` only on a real cryptographic verdict (`valid`, `bogus`, or `insecure` in permissive mode); timeout/error/throw stamps `resolver-level` or `unchecked`.
- `FallbackResolver.checkBulk` tracks `{ domain, index }` pairs end to end.
- Composition root forwards the fallback reason to logs/metrics instead of dropping it.
**Advantages:**
- Operator confusion eliminated; misconfiguration fails fast at startup.
- Audit trail trustworthy for post-incident forensics.
- Duplicate-heavy closeout CSVs no longer lose verdicts.
- Zero behavior change for the happy path; strictly safer failure modes.
**Disadvantages:**
- Operators with `DNS_FALLBACK_PROVIDER=cloudflare-doh` in `.env` will fail at boot until they set `node-dns` (intended breaking fail-closed change).
### Option B: Implement DoH fallback providers now
Rejected: DoH legs were deprecated by ADR-0075 (Unbound single source of truth).
Reintroducing them as fallback re-adds operator-diversity complexity without
DNSSEC validation, and does not fix the provenance or bulk bugs.
### Option C: Full production @relaycorp/dnssec wire adapter
Rejected for this step: requires raw DNS wire construction (DO bit, TCP
fallback, `Message.deserialise`) and a dedicated test harness against live
signed/unsigned/bogus zones. Valuable long-term, but high risk as a single
change alongside the three fail-closed fixes. Tracked as follow-up work.
## Decision
Implement Option A.
## Consequences
Positive:
- Fail-closed startup for unimplemented fallback providers.
- Honest `dnssecSource` in `DnsCheckResult` and `VerdictProvenance`.
- Correct bulk merge for duplicate domains.
- Fallback activation reason preserved in logs and `dominus_dns_fallback_active` context.
Negative:
- Intentional breaking change for `DNS_FALLBACK_PROVIDER=cloudflare-doh|google-doh` (must migrate to `node-dns`).
## Implementation
- `src/providers/dns/fallback-resolver.ts` — throw on unimplemented DoH; index-based bulk merge.
- `src/providers/dns/unbound-resolver.ts` — `perQueryValidated` provenance gate.
- `src/config.ts`, `.env.example` — document DoH as reserved/fail-closed.
- `src/app/composition-root.ts` — forward fallback reason.
- Tests: DoH fail-closed, duplicate bulk, per-query timeout vs valid provenance.
- Docs: `README.md` pipeline description corrected (Unbound, not Node dns module).