Ctrl K
ComunidadGuías…

docs/adr/0075-dns-unbound-single-source-completion.md

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

Archivo original ↗Preguntar a KiBot
---
title: DNS: UnboundResolver as Single Source of Truth (ADR-0072 Completion)
status: accepted
date: 2026-09-24
decision-makers: [Principal Software Engineer]
---

# ADR-0075: DNS: UnboundResolver as Single Source of Truth (ADR-0072 Completion)

## Context

ADR-0072 established UnboundResolver as the single DNS source of truth, but the implementation retained a dual-resolver architecture:

1. **UnboundResolver** — Primary resolver with proven DNSSEC validation via negative-control probes
2. **NodeDnsProvider** — Fallback resolver (native node:dns, DoH, DoT) used when Unbound was unhealthy or DNSSEC validation was lost

This dual architecture created several critical issues:

| Issue | Impact |
|-------|--------|
| **Ambiguous DNSSEC semantics** | Same domain could receive `dnssec: 'valid'` from Unbound but `dnssec: 'unchecked'` from NodeDnsProvider fallback |
| **Cache incoherence** | Two independent cache layers (Unbound + NodeDnsProvider) with different TTLs and invalidation logic |
| **Breaker registry sharing** | Shared `DnsBreakerRegistry` but different failure semantics per resolver type |
| **Per-query DNSSEC opt-in** | `DNS_PER_QUERY_DNSSEC=false` default left a 10-minute window where `val-permissive-mode: yes` reconfiguration could go undetected |
| **Single positive control** | Health check relied solely on `sigok.verteiltesysteme.net` — zone outage = global resolver failure |

## Decision

Complete ADR-0072 by making **UnboundResolver the exclusive `DnsProvider` implementation**:

1. **Remove `NodeDnsProvider` from `buildDnsProvider`** — No fallback path; `DNS_UNBOUND_ENABLED=false` is no longer supported
2. **Enable per-query DNSSEC validation by default** (`DNS_PER_QUERY_DNSEC=true`) — Every Available verdict gets cryptographic proof (DS → DNSKEY → RRSIG) for that specific domain
3. **Distributed positive controls** (`DNSSEC_POSITIVE_CONTROLS`) — Multiple known-good DNSSEC zones (`sigok.verteiltesysteme.net`, `dnssec.works`, `test.dnssec-tools.org`) provide geographic/topological diversity
4. **Fail-fast on health check failure** — No silent fallback; operator must fix Unbound configuration
5. **Remove fallback-specific config** — `DNS_UNBOUND_FALLBACK_ENABLED`, `DNS_UNBOUND_FALLBACK_REVALIDATION_INTERVAL_MS`, `DNS_UNBOUND_MAX_UNHEALTHY_BEFORE_FALLBACK` replaced with `DNS_UNBOUND_MAX_UNHEALTHY_BEFORE_DEGRADED`

## Configuration Changes

### New Required Settings

| Variable | Default | Description |
|----------|---------|-------------|
| `DNS_UNBOUND_HOSTS` | `127.0.0.1` | Comma-separated Unbound host(s) — **required** |
| `DNS_PER_QUERY_DNSEC` | `true` | Enable per-query DNSSEC validation for Available verdicts |
| `DNS_PER_QUERY_DNSEC_TIMEOUT_MS` | `2000` | Timeout for per-query validation |
| `DNSSEC_POSITIVE_CONTROLS` | `sigok.verteiltesysteme.net,dnssec.works,test.dnssec-tools.org` | Comma-separated positive control domains |

### Removed Settings

| Variable | Replacement |
|----------|-------------|
| `DNS_UNBOUND_FALLBACK_ENABLED` | Removed — no fallback |
| `DNS_UNBOUND_FALLBACK_REVALIDATION_INTERVAL_MS` | Removed — no fallback |
| `DNS_UNBOUND_MAX_UNHEALTHY_BEFORE_FALLBACK` | → `DNS_UNBOUND_MAX_UNHEALTHY_BEFORE_DEGRADED` |

### Behavior Changes

| Scenario | Old Behavior | New Behavior |
|----------|--------------|--------------|
| Unbound unhealthy at boot | Fallback to NodeDnsProvider (warning) | **Boot fails** with actionable error |
| DNSSEC validation lost at runtime | Activate fallback (warning) | **Log error**, mark resolver degraded, continue serving with `dnssec: 'unchecked'` |
| Per-query DNSSEC timeout | N/A (disabled by default) | Returns `dnssec: 'valid'` from resolver-level stamp (fail-open for availability) |
| Positive control zone unreachable | Global resolver failure | Try next positive control; only fail if all exhausted |

## Consequences

### Positive

- **Single source of truth** for DNS — no ambiguity about which resolver answered
- **Cryptographic proof per Available verdict** — closes the `val-permissive-mode` window (ADR-0073)
- **Resilience through diversity** — multiple positive controls survive single-zone outages
- **Simpler architecture** — one resolver, one cache, one breaker registry, one code path
- **Fail-fast operational model** — operator alerted immediately on resolver issues

### Negative

- **Unbound becomes mandatory** — Community edition must run Unbound locally (Docker or host)
- **No graceful degradation** — Unbound misconfiguration = hard boot failure
- **Per-query DNSSEC latency** — ~50-200ms additional per Available domain (mitigated by resolver-level stamp fail-open)

## Migration Guide

### Community Edition (no Docker)

```bash
# Install Unbound locally
# Ubuntu/Debian:
sudo apt-get install unbound

# Configure /etc/unbound/unbound.conf:
#   server:
#     validator: yes
#     val-permissive-mode: no
#     forward-zone:
#       name: "."
#       forward-tls-upstream: yes
#       forward-addr: 1.1.1.1@853  # or your preferred DoT upstream

# Set in .env:
DNS_UNBOUND_HOSTS=127.0.0.1
DNS_PER_QUERY_DNSEC=true
```

### Docker Compose

```yaml
services:
  unbound:
    image: mvhun/unbound:latest
    volumes:
      - ./deploy/unbound/unbound.conf:/etc/unbound/unbound.conf:ro
    ports:
      - "53:53/tcp"
      - "53:53/udp"
    restart: unless-stopped

  app:
    environment:
      - DNS_UNBOUND_HOSTS=unbound
      - DNS_PER_QUERY_DNSEC=true
```

### Verification

```bash
# Health check
curl http://localhost:3000/api/v1/health | jq '.dns.dnssecValid'
# Should return true

# Test per-query DNSSEC
dominus score available-domain.example.com | jq '.scoreResult.breakdown.intrinsic.dnssec'
# Should show "valid" for Available domains
```

## Related ADRs

- **ADR-0072** — Unbound Resolver as Single Source of Truth (superseded/completed)
- **ADR-0073** — DNSSEC Per-Query Validation (now default ON)
- **ADR-0061** — DNSSEC Fail-Closed (reinforced)
- **ADR-0059** — DNS Circuit Breaker (simplified to single resolver)

## Rollback Plan

If critical issues arise, temporarily re-enable fallback by:

1. Reverting `provider-factory.ts` to previous version
2. Setting `DNS_UNBOUND_ENABLED=false` (will use NodeDnsProvider)
3. This is a **temporary measure only** — architecture decision is final