docs/adr/0072-dns-consensus-revalidation-hardening.md
Rama master · réplica de lectura del repositorio original.
# ADR-0072: DNS Consensus Revalidation Hardening — Strict Anycast Overlap Veto with System Resolver Independence
**Status**: Accepted
**Date**: 2026-09-07
**Supersedes**: ADR-0039, 0040, 0042, 0044, 0045, 0047, 0048, 0059, 0063, 0065, 0068, 0069 (legacy consensus topology)
**Related**: ADR-0002, ADR-0039, ADR-0044, ADR-0045, ADR-0059, ADR-0063, ADR-0064, ADR-0065, ADR-0066, ADR-0068, ADR-0069
## Context
The DNS 2-of-3 consensus gate (ADR-0039, ADR-0044, ADR-0045) implements periodic runtime revalidation of resolver disjointness via `revalidateDisjointness()` (consensus-engine.ts). This revalidation issues live DNS queries through each consensus leg's resolver groups to detect anycast/IP overlap that static bootstrap checks cannot catch.
**Critical flaw identified**: The revalidation was using each leg's *own pinned resolvers* to resolve the endpoint hostnames. When primary and secondary both point at the same private recursor (e.g., `127.0.0.1:5300`), the revalidation would query that same recursor to resolve `127.0.0.1:5300` — which trivially returns the same IP, *masking* the overlap instead of detecting it. The gate was effectively "consensus theater" in privacy-mode deployments with a single shared recursor.
Additionally, the revalidation lacked Prometheus metrics, making it impossible to alert on degradation or track revalidation health over time.
## Decision
1. **Strict system-resolver independence**: `revalidateDisjointness()` now uses the *system resolver* (Node.js default `dns.Resolver()` without pinned nameservers) to resolve the endpoint hostnames of each leg. This ensures that even if primary and secondary share a pinned recursor, the revalidation will independently resolve their hostnames and detect the anycast overlap.
2. **Fail-open for resolution, fail-closed for overlap**: Resolution failures (timeout, NXDOMAIN, SERVFAIL) during revalidation are logged as warnings but do NOT mark the gate as degraded. Only actual IP overlap exceeding the threshold (default 50%) triggers `config.runtimeDegraded = true` and `config.anycastDegraded = true`.
3. **Prometheus observability**: Added `DnsConsensusRevalidationMetrics` interface and wired it through `ConsensusDnsProvider` → `buildConsensusDnsProvider()` → composition root. Metrics recorded:
- `revalidation:anycast-overlap` when overlap exceeds threshold
- `revalidation:error:<message>` when revalidation throws
- Uses existing `recordDnsConsensusDegradedReason()` for consistency
4. **Fallback group exclusion**: Revalidation continues to skip `fallback: true` groups (same as bootstrap validation), so emergency fallbacks shared between legs don't trigger false degradation.
5. **Privacy-mode compliance**: The system-resolver approach works correctly with `DNS_PRIVACY_MODE=true` — the revalidation queries never leave the host except to the system resolver, maintaining the privacy guarantee while still detecting overlap.
### Completion (2026-09-21)
**The legacy 2-of-3 DNS consensus architecture has been fully removed** in favor of **Unbound as the single source of truth** (ADR-0072):
- **Legacy code removed**: `ConsensusDnsProvider`, `consensus-engine.ts`, `createDnsConsensusRateLimiter`, `recordDnsConsensus` from MetricsCollector, `DnsConsensusMetrics` from types, all DNS consensus Prometheus metrics exports
- **Default confirmed**: `DNS_UNBOUND_ENABLED=true` (was `false`) — the hardened architecture is now the only path
- **Periodic DNSSEC revalidation added**: UnboundResolver now runs `revalidateDnssecValidation()` every 10 minutes (configurable via `dnssecRevalidationIntervalMs`), invalidates memory cache on validation state change, emits `dominus_unbound_dnssec_validation_lost_total` and `dominus_unbound_dnssec_validation_recovered_total` counters for Prometheus alerting
- **RDAP consensus WHOIS rescue isolated**: Dedicated rescue provider with separate rate limiter namespace (`rdap-consensus-whois-rescue`) avoids head-of-line blocking on slow ccTLD WHOIS servers (.it, .de, .jp, .br)
- **Intra-stage checkpointing enabled**: Default `PIPELINE_CHECKPOINT_BATCH_SIZE=500` enables mid-stage resume capability for large pipeline runs
- **Prometheus alert rules added**:
- `UnboundDnssecValidationLost` (critical): DNSSEC validation lost via periodic revalidation
- `UnboundDnssecValidationRecovered` (info): DNSSEC validation regained
- `RdapBootstrapFailuresHigh` (warning): IANA RDAP bootstrap consecutive failures >3
- `RdapBootstrapDown` (critical): Bootstrap refresh unsuccessful
- `PipelineRunDegraded` (warning): Pipeline runs completing in degraded mode
- `PipelineStageTimeoutHigh` (warning): Stage timeout rate >10%
- **All tests pass**: 2362 backend + 379 frontend, TypeScript strict, ESLint clean
- **Migration complete**: The consensus config (`DNS_CONSENSUS_*`, `DNS_TERTIARY_*`) is now dead code and will be removed from config schema in v1.2.0
## Consequences
### Positive
- **True anycast detection**: Overlap between primary/secondary/tertiary legs is now reliably detected even when they share pinned recursors.
- **ADR-0002 conservatism upheld**: False Available verdicts from rubber-stamp second opinions are prevented.
- **Observable degradation**: Operators can alert on `dominus_dns_consensus_degraded_reason{reason="revalidation:anycast-overlap"}` and track revalidation health.
- **Fail-safe defaults**: Resolution failures don't cause false degradation; only genuine overlap does.
### Negative
- **Extra system resolver queries**: Each revalidation cycle issues 2-6 additional DNS queries (A + AAAA per endpoint hostname). At 10-min interval with 3-5 endpoints per leg, this is negligible (<1 qps).
- **System resolver dependency**: If the system resolver is misconfigured or blocked, revalidation will log warnings but not degrade the gate (fail-open for resolution).
### Neutral
- No change to bootstrap-time disjointness checks (`validateConsensusDisjointnessRuntime`).
- No change to consensus verdict logic (`runConsensus`).
- Existing `DNS_CONSENSUS_ON_FAILURE=degraded-anycast` policy continues to work with the new anycast data.
## Implementation Files
- `src/providers/dns/consensus-engine.ts`: Core revalidation logic, metrics interface
- `src/providers/dns/consensus-dns-provider.ts`: Metrics callback wiring
- `src/app/provider-factory.ts`: Factory parameter propagation
- `src/app/composition-root.ts`: Metrics collector wiring
- `src/providers/dns/__tests__/consensus-revalidation-hardening.test.ts`: Comprehensive test coverage (8 tests)
## Verification
- All new tests pass (8/8) covering: same-recursor detection, anycast overlap detection, resolution failure handling, fallback exclusion, privacy-mode resolution.
- Full test suite: 2717 passed (1 pre-existing flaky test in `dot-pool.test.ts` unrelated).
- TypeScript strict mode: clean.
- ESLint: clean.
- Pre-commit hooks: pass.
## Rollback
If issues arise, set `DNS_CONSENSUS_RUNTIME_VALIDATION=false` to disable periodic revalidation entirely, or `DNS_CONSENSUS_ON_FAILURE=disable` to disable the consensus gate on bootstrap failure. The metrics callback is optional and fails gracefully if not provided.