Índice / Módulo 2 — Hardening del Operador

2.4 DNS propio y resolución privada

Cada nombre de dominio que resuelves — cada sitio que visitas, cada servicio al que te conectas — pasa primero por una consulta DNS. Si esa consulta va a un servidor de terceros, alguien sabe exactamente qué visitas y cuándo. Controlar tu propio resolver es el primer paso para que eso no ocurra.

2.4.1 Montar un resolver recursivo propio: Unbound, sin logs

La mayoría de sistemas usan un stub resolver: el sistema operativo reenvía todas las consultas DNS a un servidor configurado por DHCP (normalmente el del router, que a su vez usa el del ISP o el de Google/Cloudflare). Eso significa que un tercero ve cada consulta que haces.

Un resolver recursivo como Unbound resuelve los nombres directamente: consulta los root servers, sigue la cadena de delegación hasta el servidor autoritativo y devuelve la respuesta. Nadie en medio ve el historial completo de tus consultas. El resolver vive en tu máquina o en tu red local y no tiene logs.

Instalación y configuración base

bash — instalar Unbound
sudo apt install -y unbound

# Verificar que arranca correctamente
sudo systemctl status unbound

# Probar que resuelve antes de tocar nada
dig @127.0.0.1 example.com
# Si devuelve respuesta, Unbound funciona en localhost

Configuración completa sin logs

bash — /etc/unbound/unbound.conf
server:
    # Escuchar solo en localhost (no exponer a la red)
    interface: 127.0.0.1
    port: 53

    # Solo aceptar consultas desde localhost
    access-control: 127.0.0.0/8 allow
    access-control: 0.0.0.0/0 refuse

    # ── LOGS: desactivar completamente ──
    verbosity: 0
    logfile: ""
    log-queries: no
    log-replies: no
    log-tag-queryreply: no
    log-local-actions: no

    # ── PRIVACIDAD ──
    # No enviar la versión de Unbound en respuestas
    hide-identity: yes
    hide-version: yes

    # Minimización de consultas QNAME (RFC 7816)
    # Solo envía la parte necesaria del nombre a cada servidor
    # en vez de el nombre completo, reduciendo la información expuesta
    qname-minimisation: yes

    # Aleatorizar el puerto de origen para dificultar ataques de envenenamiento
    # de caché (Kaminsky attack)
    port-range-low: 1024
    port-range-high: 65535

    # ── SEGURIDAD ──
    # Validar DNSSEC: rechaza respuestas sin firma válida
    auto-trust-anchor-file: "/var/lib/unbound/root.key"

    # No seguir redireccionamientos a IPs privadas (DNS rebinding)
    private-address: 192.168.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8
    private-address: 127.0.0.0/8

    # ── RENDIMIENTO ──
    # Caché: mantener respuestas para no re-consultar
    cache-max-ttl: 86400
    cache-min-ttl: 300
    msg-cache-size: 64m
    rrset-cache-size: 128m

    # Prefetch: renovar entradas populares antes de que expiren
    prefetch: yes
    prefetch-key: yes

    # Usar múltiples threads para mejor rendimiento
    num-threads: 2
bash — aplicar la configuración y actualizar el anchor DNSSEC
# Descargar la clave raíz de DNSSEC (necesaria para validación)
sudo -u unbound unbound-anchor -a /var/lib/unbound/root.key

# Verificar que la configuración no tiene errores de sintaxis
sudo unbound-checkconf

# Reiniciar Unbound para aplicar cambios
sudo systemctl restart unbound
sudo systemctl enable unbound

# Configurar el sistema para usar Unbound como resolver
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf

# En sistemas con systemd-resolved activo, desactivarlo primero
sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
# Hacer el archivo inmutable para que DHCP no lo sobreescriba
sudo chattr +i /etc/resolv.conf

Verificar que funciona correctamente

bash — pruebas de funcionamiento
# Resolución básica
dig example.com
# SERVER: 127.0.0.1#53  → usa Unbound local

# Verificar que DNSSEC valida correctamente
dig sigfail.verteiltesysteme.net
# Debe devolver SERVFAIL (firma inválida rechazada)

dig dnssec-tools.org
# Debe devolver respuesta normal con la flag "ad" (Authenticated Data)

# Verificar que no hay logs generándose
sudo journalctl -u unbound --since "1 minute ago"
# Solo debe mostrar arranque del servicio, sin queries

# Estadísticas de la caché (sin revelar qué se consultó)
sudo unbound-control stats_noreset | grep "total\."
⚠️
DHCP sobreescribe /etc/resolv.conf: Al reconectarte a una red, el cliente DHCP suele sobreescribir /etc/resolv.conf con los servidores DNS del router. El chattr +i previene esto, pero si cambias de red y tienes problemas de DNS, puede ser que el archivo esté protegido. Para NetworkManager hay una opción más limpia: en el archivo de conexión, añadir dns=none en la sección [main].

2.4.2 Pi-hole como filtro de telemetría en tu red local

Pi-hole es un servidor DNS que bloquea dominios en listas negras antes de resolver la consulta. Cualquier dispositivo de tu red que lo use como resolver no podrá conectarse a dominios de telemetría, tracking, publicidad y malware conocidos — sin instalar nada en cada dispositivo. Una sola instancia protege toda la red.

La combinación más potente para OPSEC es Pi-hole como primer filtro y Unbound como resolver recursivo upstream — Pi-hole bloquea los dominios indeseados y Unbound resuelve el resto sin logs y con validación DNSSEC.

Instalación en Raspberry Pi o servidor local

bash — instalar Pi-hole
# Instalación con el script oficial (verificar la firma antes de ejecutar)
curl -sSL https://install.pi-hole.net -o pihole_install.sh
# Revisar el script antes de ejecutarlo
less pihole_install.sh
bash pihole_install.sh

# O con Docker Compose (opción más limpia y reproducible)
cat > docker-compose.yml << 'EOF'
services:
  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "8080:80/tcp"
    environment:
      TZ: 'Europe/Madrid'
      WEBPASSWORD: 'tu_contraseña_admin'
      PIHOLE_DNS_: '127.0.0.1#5335'
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    restart: unless-stopped
    network_mode: host
EOF
docker compose up -d

Integrar Pi-hole con Unbound como upstream

Cuando Pi-hole no tiene la respuesta en caché ni la bloquea, reenvía la consulta upstream. Si configuramos Unbound en el puerto 5335 como upstream de Pi-hole, la cadena completa es: dispositivo → Pi-hole (filtrado) → Unbound (resolución recursiva sin logs).

bash — configurar Unbound en puerto 5335 para Pi-hole
# Crear configuración específica de Unbound para Pi-hole
sudo nano /etc/unbound/unbound.conf.d/pi-hole.conf

# Contenido:
server:
    # Puerto diferente al 53 para no colisionar con Pi-hole
    port: 5335
    interface: 127.0.0.1

    verbosity: 0
    logfile: ""
    log-queries: no

    qname-minimisation: yes
    hide-identity: yes
    hide-version: yes

    auto-trust-anchor-file: "/var/lib/unbound/root.key"

    prefetch: yes
    cache-max-ttl: 86400

sudo systemctl restart unbound

# Verificar que Unbound escucha en el puerto 5335
ss -tlnp | grep 5335

# Configurar Pi-hole para usar Unbound como upstream
# En la interfaz web: Settings → DNS → Custom DNS servers
# Añadir: 127.0.0.1#5335
# Desmarcar todos los DNS públicos de la lista

Listas de bloqueo para telemetría

Las listas por defecto de Pi-hole bloquean publicidad, pero para telemetría del sistema operativo y aplicaciones necesitas listas más específicas:

bash — añadir listas de bloqueo desde la CLI
# Añadir listas directamente con pihole
pihole -a adlist add https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
pihole -a adlist add https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt
pihole -a adlist add https://raw.githubusercontent.com/nicehash/block-list/main/list.txt

# Actualizar todas las listas y regenerar la base de datos
pihole -g

# Ver estadísticas de bloqueo
pihole -c

# Ver los últimos dominios bloqueados
pihole -t | grep "blocked"

# Desactivar los logs de queries de Pi-hole también
# En Settings → Privacy → Privacy level → Anonymous mode
# O desde CLI:
pihole logging off
💡
Pi-hole en tu máquina local, no en un servidor externo: Algunos proveedores ofrecen "Pi-hole as a service". Eso derrota completamente el propósito — estás enviando todas tus consultas a otro tercero. Pi-hole solo tiene sentido ejecutado en hardware que controlas tú: una Raspberry Pi, un servidor local, o en Docker en tu propio equipo.

2.4.3 DNS-over-Tor: resolución completamente anónima

Unbound sin logs resuelve el problema de qué registra tu resolver, pero los root servers y los servidores autoritativos aún ven tu IP cuando les consultas. DNS-over-Tor enruta todas las consultas DNS a través de la red Tor, de forma que ningún servidor DNS ve tu IP real — solo ven la IP del nodo de salida de Tor.

Es la opción para escenarios donde el anonimato de las consultas DNS es crítico, no solo la privacidad frente al ISP.

Opción 1: usar el DNSPort de Tor directamente

Tor incluye un puerto DNS integrado que resuelve nombres a través de la red Tor. Es la configuración más simple.

bash — activar DNSPort en Tor
# Instalar Tor si no está
sudo apt install -y tor

# Añadir estas líneas a /etc/tor/torrc
echo "DNSPort 127.0.0.1:9053" | sudo tee -a /etc/tor/torrc
# DNSPort 9053: Tor escuchará consultas DNS en el puerto 9053 de localhost
# y las resolverá a través de la red Tor

sudo systemctl restart tor

# Verificar que el puerto 9053 está activo
ss -tlnp | grep 9053

# Probar: resolver un dominio a través de Tor
dig -p 9053 @127.0.0.1 check.torproject.org
# Debe devolver una IP del nodo de salida de Tor, no tu IP real

# Configurar el sistema para usar el DNSPort de Tor
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
# Si Tor usa el puerto 9053 en vez del 53, necesitas un redirecionador
# La opción más limpia: usar iptables para redirigir el puerto 53 al 9053
sudo iptables -t nat -A OUTPUT -d 127.0.0.1 -p udp --dport 53 -j REDIRECT --to-ports 9053
sudo iptables -t nat -A OUTPUT -d 127.0.0.1 -p tcp --dport 53 -j REDIRECT --to-ports 9053

Opción 2: Unbound enrutando consultas a través de SOCKS de Tor

Esta configuración es más robusta: Unbound sigue siendo el resolver local, pero en lugar de consultar los root servers directamente, enruta todas sus consultas a través del proxy SOCKS5 de Tor. Mantiene las ventajas de Unbound (caché, DNSSEC, sin logs) añadiendo el anonimato de Tor.

bash — Unbound con forward a través del SOCKS5 de Tor
# Verificar que Tor está corriendo con SocksPort activo (por defecto: 9050)
ss -tlnp | grep 9050

# Añadir esta sección al final de /etc/unbound/unbound.conf
# Reenviar todas las consultas a un resolver público accessible via Tor

forward-zone:
    name: "."
    forward-addr: 9.9.9.9@853   # Quad9, pero la conexión viaja por Tor
    forward-tls-upstream: yes

# ── O usar el hidden service DNS de Tor directamente ──
# Algunas instancias de Quad9 y Cloudflare tienen .onion addresses
# Configurar Unbound para resolver .onion via Tor primero

# Solución más directa: usar torsocks para envolver dig y herramientas DNS
torsocks dig +short myip.opendns.com @resolver1.opendns.com
# La IP que ve el servidor DNS es la del nodo de salida Tor, no la tuya

# Forzar que TODAS las consultas DNS del sistema pasen por Tor
# con Transparent Proxy de Tor (requiere también redirigir el tráfico)
sudo nano /etc/tor/torrc
# Añadir:
# DNSPort 0.0.0.0:53
# TransPort 0.0.0.0:9040
# VirtualAddrNetworkIPv4 10.192.0.0/10
# AutomapHostsOnResolve 1

Limitaciones de DNS-over-Tor

  • Latencia: cada consulta DNS añade la latencia de tres saltos en la red Tor. Para navegación normal puede ser aceptable; para aplicaciones que hacen cientos de consultas por sesión puede notarse.
  • Sin DNSSEC en DNSPort: el DNSPort integrado de Tor no valida DNSSEC. Si usas la Opción 1, pierdes la validación de firmas. La Opción 2 con Unbound mantiene DNSSEC.
  • Los nodos de salida Tor ven las consultas en texto plano: a menos que uses DoT/DoH hacia el servidor upstream, el nodo de salida ve el nombre del dominio. Combinar Tor con un resolver DoT mitiga esto.

2.4.4 Verificación de que ninguna consulta sale del túnel

Configurar Unbound, Pi-hole o DNS-over-Tor no garantiza que funcionen correctamente. Un error de configuración, una aplicación que ignora el resolver del sistema, o una fuga IPv6 pueden hacer que las consultas DNS salgan por fuera del túnel sin que te des cuenta. La verificación activa es imprescindible.

Monitorizar el tráfico DNS con tcpdump

El método más directo: capturar todo el tráfico DNS que sale de la interfaz de red real y verificar que no hay ninguna consulta que no debería estar ahí.

bash — capturar consultas DNS con tcpdump
# Ver todo el tráfico DNS (puerto 53) en la interfaz física
# eth0 o la interfaz de tu red real (no lo, no docker0, etc.)
sudo tcpdump -i eth0 -n 'port 53'

# Si Unbound está configurado correctamente:
# → Verás tráfico al puerto 53 DESDE Unbound hacia root servers y autoritativos
# → NO deberías ver tráfico DNS hacia 8.8.8.8, 1.1.1.1 u otros resolvers públicos
# → NO deberías ver consultas DNS del usuario directamente

# Si usas VPN o Tor: NO debería haber NINGÚN tráfico DNS fuera del túnel
sudo tcpdump -i eth0 -n 'port 53 or port 853'
# Con VPN activa, este comando no debería mostrar NADA
# Toda la resolución debe ir por la interfaz de la VPN (tun0, wg0, etc.)

# Capturar también en IPv6 por si hay fugas por esa vía
sudo tcpdump -i eth0 -n 'ip6 and port 53'

# Hacer una consulta de prueba mientras tcpdump escucha
# (desde otro terminal)
dig test-fuga-dns.com

Verificar el resolver activo con dig

bash — comprobar qué servidor resuelve tus consultas
# Ver qué servidor DNS está usando el sistema
cat /etc/resolv.conf
# Debe mostrar: nameserver 127.0.0.1

# Confirmar con dig qué servidor responde
dig example.com
# Buscar la línea "SERVER:" en la respuesta
# SERVER: 127.0.0.1#53  → correcto, usa Unbound local
# SERVER: 8.8.8.8#53   → MAL, consulta va a Google directamente

# Comprobar qué IP ve el servidor DNS al recibir tu consulta
# (el dominio o.myaddr.l.google.com devuelve la IP de quien consulta)
dig TXT o-o.myaddr.l.google.com @ns1.google.com +short
# → Debe devolver la IP de tu resolver (Unbound), NO tu IP pública
# → Con DNS-over-Tor: debe devolver una IP del nodo de salida Tor

# Resolver alternativo para identificar fugas: whoami.akamai.net
dig +short myip.opendns.com @resolver1.opendns.com
# La IP devuelta es la que ve el servidor DNS upstream
# Con Unbound: tu IP pública (Unbound hace consultas en tu nombre)
# Con Tor: IP del nodo de salida
# Con VPN: IP de la VPN

Detectar fugas DNS con VPN activa

bash — verificación completa de fugas con VPN
# Script de verificación rápida de fugas DNS
#!/bin/bash
# check_dns_leaks.sh

echo "=== Resolver configurado en el sistema ==="
cat /etc/resolv.conf

echo ""
echo "=== IP que ve el servidor DNS (lo que identifica tu resolver) ==="
dig +short myip.opendns.com @resolver1.opendns.com

echo ""
echo "=== Tráfico DNS activo en la interfaz física (10 segundos) ==="
echo "Lanzando consulta de prueba en segundo plano..."
dig test.fuga-comprobacion.org >/dev/null &
sudo timeout 10 tcpdump -i eth0 -n 'port 53' 2>/dev/null
echo ""
echo "=== Si la VPN está activa y no hay fugas, no se ve tráfico DNS arriba ==="

# Comprobar interfaces de red activas
echo ""
echo "=== Interfaces activas ==="
ip -br addr | grep UP

# Comprobar tablas de routing (el DNS no debería tener ruta fuera del túnel)
echo ""
echo "=== Tabla de rutas ==="
ip route show

El caso especial de IPv6

Las fugas DNS por IPv6 son la trampa más común: se configura correctamente el DNS en IPv4 pero el sistema tiene IPv6 activo y hace consultas DNS por esa vía que salen directamente, ignorando la VPN o el túnel.

bash — desactivar IPv6 si no lo necesitas
# Ver si hay consultas DNS saliendo por IPv6
sudo tcpdump -i eth0 -n 'ip6 and port 53'

# Desactivar IPv6 completamente en el kernel si no lo usas
cat > /etc/sysctl.d/disable-ipv6.conf << 'EOF'
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
EOF
sudo sysctl -p /etc/sysctl.d/disable-ipv6.conf

# Verificar que IPv6 está desactivado
ip addr show | grep inet6
# Solo debe aparecer ::1/128 (loopback) si está desactivado para interfaces

# Añadir también a /etc/resolv.conf para no hacer consultas AAAA innecesarias
# options inet6 → quitar esta opción si está presente

# En Unbound, deshabilitar también la escucha en IPv6
# En unbound.conf:
# do-ip6: no
💡
La verificación debe ser habitual, no puntual: Una configuración correcta hoy puede romperse mañana con una actualización del sistema, un cambio de red o un reinicio en el orden incorrecto de los servicios. Incorpora el script de verificación como parte del ritual de inicio de cualquier sesión donde el DNS privado sea crítico.