DNS-over-TLS (DoT) y DNS-over-HTTPS (DoH) cifran las consultas DNS entre tus dispositivos y el resolvedor, de modo que ni la red Wi-Fi en la que estés ni tu proveedor de acceso pueden leerlas o modificarlas. En este tutorial montarás tu propio resolvedor cifrado en Ubuntu 24.04 con Unbound, que desde la versión 1.17 sirve DoT y DoH de forma nativa, sin proxies intermedios. Al terminar, tus dispositivos podrán usar dns.your_domain como DNS cifrado desde cualquier red.

DoT y DoH en resumen

Los dos protocolos envían el mismo mensaje DNS dentro de una conexión TLS. Cambia el transporte:

DNS-over-TLS (DoT)DNS-over-HTTPS (DoH)
Puerto853/TCP443/TCP
TransporteTLS directoHTTP/2 sobre TLS
Soporte típicoAndroid (DNS privado), systemd-resolvedNavegadores, Windows 11, iOS y macOS
Bloqueo por la redFácil de detectar y bloquear por su puertoSe confunde con el tráfico web normal

Unbound puede servir ambos a la vez, cada uno en su puerto, y resolver las consultas de forma recursiva con validación DNSSEC.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS y una IP pública, por ejemplo un VPS de CubePath, con al menos 1 GB de RAM.
  • Un usuario no root con privilegios sudo.
  • Un subdominio, en los ejemplos dns.your_domain, con un registro A (y AAAA si usas IPv6) que apunte a la IP del servidor, your_server_ip.
  • Los puertos 80, 443 y 853 libres: este servidor no debe tener ya un servidor web en el 443.

Paso 1: Instalar Unbound y Certbot

Instala Unbound, Certbot para el certificado y dnsutils para las pruebas:

sudo apt update
sudo apt install unbound certbot dnsutils

DoH necesita que Unbound esté compilado con la biblioteca nghttp2. El paquete de Ubuntu 24.04 lo está; compruébalo:

unbound -V | grep -o 'with-libnghttp2'
with-libnghttp2

Si el comando no devuelve nada, tu versión de Unbound solo podrá servir DoT.

Paso 2: Obtener el certificado TLS

Los clientes solo aceptarán el servidor si presenta un certificado válido para dns.your_domain. Abre el puerto 80 para la validación de Let's Encrypt, junto con SSH:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw enable

Solicita el certificado. El modo standalone levanta un servidor web temporal en el puerto 80 durante la validación:

sudo certbot certonly --standalone -d dns.your_domain
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/dns.your_domain/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/dns.your_domain/privkey.pem

Unbound no puede leer los ficheros de /etc/letsencrypt, que solo son accesibles para root, y además el perfil de AppArmor de Unbound puede impedirle leer fuera de /etc/unbound. La solución es copiar el certificado a /etc/unbound/tls con permisos para el grupo unbound cada vez que se renueve. Crea para ello un deploy hook de Certbot:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/unbound.sh
#!/usr/bin/env bash
set -euo pipefail

domain="dns.your_domain"
src="/etc/letsencrypt/live/${domain}"
dst="/etc/unbound/tls"

install -d -m 0750 -o root -g unbound "$dst"
install -m 0644 -o root -g unbound "${src}/fullchain.pem" "${dst}/fullchain.pem"
install -m 0640 -o root -g unbound "${src}/privkey.pem" "${dst}/privkey.pem"

systemctl restart unbound

Hazlo ejecutable y ejecútalo una vez para hacer la primera copia:

sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/unbound.sh
sudo /etc/letsencrypt/renewal-hooks/deploy/unbound.sh
sudo ls -l /etc/unbound/tls
-rw-r--r-- 1 root unbound 5212 Sep 25 10:40 fullchain.pem
-rw-r----- 1 root unbound  241 Sep 25 10:40 privkey.pem

Certbot ejecuta los scripts de renewal-hooks/deploy/ tras cada renovación, que gestiona el temporizador certbot.timer. Comprueba que la renovación funcionará:

sudo certbot renew --dry-run

Paso 3: Configurar Unbound para DoT y DoH

Crea un fichero de configuración propio. Ubuntu carga automáticamente todo lo que haya en /etc/unbound/unbound.conf.d/, donde ya están activadas la validación DNSSEC y unbound-control:

sudo nano /etc/unbound/unbound.conf.d/encrypted-dns.conf
server:
    # DNS sin cifrar solo para el propio servidor
    interface: 127.0.0.1
    interface: ::1

    # DNS-over-TLS
    interface: 0.0.0.0@853
    interface: ::0@853
    # DNS-over-HTTPS
    interface: 0.0.0.0@443
    interface: ::0@443

    tls-port: 853
    https-port: 443
    http-endpoint: "/dns-query"
    tls-service-pem: "/etc/unbound/tls/fullchain.pem"
    tls-service-key: "/etc/unbound/tls/privkey.pem"

    # Acepta consultas de cualquier IP (solo llegan por TLS/HTTPS)
    access-control: 0.0.0.0/0 allow
    access-control: ::/0 allow

    # Más conexiones TCP simultáneas: cada cliente DoT/DoH mantiene una abierta
    incoming-num-tcp: 100

    # Límite de consultas por segundo por IP de cliente
    ip-ratelimit: 100

    hide-identity: yes
    hide-version: yes

    prefetch: yes
    msg-cache-size: 64m
    rrset-cache-size: 128m

Si el servidor no tiene IPv6, elimina las líneas interface: ::0@853 y interface: ::0@443.

Por qué es seguro access-control: 0.0.0.0/0 allow aquí: el puerto 53 sin cifrar solo escucha en localhost, así que desde fuera solo se puede consultar por TLS. Al ir sobre TCP, un atacante no puede falsificar la IP de origen para usar tu servidor en ataques de amplificación. Aun así, cualquiera que conozca el nombre podría usarlo; si tus clientes tienen IP fijas, sustituye esas dos líneas por reglas allow solo para ellas.

Valida la configuración y reinicia:

sudo unbound-checkconf
sudo systemctl restart unbound
unbound-checkconf: no errors in /etc/unbound/unbound.conf

Comprueba que Unbound escucha en los tres puertos:

sudo ss -ltnp | grep unbound

Debes ver sockets en 127.0.0.1:53, 0.0.0.0:853 y 0.0.0.0:443 (y sus equivalentes IPv6).

Paso 4: Abrir los puertos de DoT y DoH

sudo ufw allow 853/tcp
sudo ufw allow 443/tcp
sudo ufw status
To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
853/tcp                    ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere

El puerto 80 debe seguir abierto para que Certbot pueda renovar el certificado en modo standalone.

Paso 5: Probar el servidor

Haz estas pruebas desde otro equipo, no desde el propio servidor. Primero, confirma que el certificado es correcto en el puerto 853:

openssl s_client -connect dns.your_domain:853 -servername dns.your_domain -brief < /dev/null

La salida debe incluir Verification: OK y Protocol version: TLSv1.3.

Para probar DoT, instala kdig (paquete knot-dnsutils en Ubuntu y Debian) y haz una consulta validando el certificado:

kdig @dns.your_domain +tls-ca +tls-hostname=dns.your_domain cubepath.com

La respuesta debe mostrar una línea ;; TLS session (TLS1.3) y la sección ANSWER con la IP de cubepath.com.

Para probar DoH, envía con curl la consulta de ejemplo del RFC 8484, que pide el registro A de www.example.com codificado en base64url. Unbound solo habla HTTP/2 para DoH:

curl -s --http2 -o /dev/null -w '%{http_version} %{http_code}\n' \
  -H 'accept: application/dns-message' \
  'https://dns.your_domain/dns-query?dns=AAABAAABAAAAAAAAA3d3dwdleGFtcGxlA2NvbQAAAQAB'
2 200

Un 200 sobre HTTP versión 2 confirma que DoH responde. Por último, comprueba que el puerto 53 no es accesible desde fuera; este comando debe fallar por tiempo de espera:

dig @your_server_ip cubepath.com +time=3 +tries=1
;; communications error to your_server_ip#53: timed out

Paso 6: Configurar los clientes

Linux con systemd-resolved (DoT)

systemd-resolved necesita la IP del servidor y, tras #, el nombre que debe comprobar en el certificado:

sudo mkdir -p /etc/systemd/resolved.conf.d
sudo nano /etc/systemd/resolved.conf.d/dot.conf
[Resolve]
DNS=your_server_ip#dns.your_domain
DNSOverTLS=yes
Domains=~.
sudo systemctl restart systemd-resolved
resolvectl status | grep -E 'DNSOverTLS|Current DNS'
resolvectl query cubepath.com

La primera salida debe mostrar +DNSOverTLS y Current DNS Server: your_server_ip#dns.your_domain.

Android (DoT)

En Ajustes > Redes e Internet > DNS privado, elige Nombre del host del proveedor de DNS privado y escribe dns.your_domain. El nombre exacto del menú varía según el fabricante.

  • Firefox: Ajustes > Privacidad y seguridad > DNS sobre HTTPS > Protección máxima > Personalizado: https://dns.your_domain/dns-query.
  • Chrome y Edge: Configuración > Privacidad y seguridad > Seguridad > Usar DNS seguro > Personalizado, con la misma URL.

Windows 11, iOS y macOS

Windows 11 permite configurar DoH por adaptador de red en Configuración > Red e Internet > Propiedades del adaptador > Asignación de servidor DNS, indicando la IP del servidor y la plantilla https://dns.your_domain/dns-query. En iOS y macOS, DoH y DoT se configuran a nivel de sistema con un perfil de configuración .mobileconfig.

Solución de problemas

  • Unbound no arranca y el log habla del certificado: revisa sudo journalctl -u unbound -n 50. Suele ser que no existe /etc/unbound/tls porque no se ejecutó el deploy hook del paso 2, o que la clave no es legible para el grupo unbound.
  • "can't bind socket: Address already in use" en el puerto 443: hay un servidor web (Nginx, Apache) usando el 443. Identifícalo con sudo ss -ltnp 'sport = :443'. En ese caso, sirve solo DoT quitando las líneas @443 o usa otro servidor para DoH.
  • El cliente dice que el certificado no es válido: el cliente debe conectarse por nombre (dns.your_domain), no por IP, y el nombre debe coincidir con el del certificado.
  • curl devuelve un error de HTTP/1.1: Unbound no acepta DoH sobre HTTP/1.1. Usa una versión de curl compilada con HTTP/2.

Conclusión

Ya tienes un resolvedor propio que sirve DNS-over-TLS en el puerto 853 y DNS-over-HTTPS en el 443, con validación DNSSEC y un certificado de Let's Encrypt que se renueva y se copia solo. Como siguientes pasos puedes añadir zonas locales o bloqueo de dominios con local-zone en Unbound, o poner AdGuard Home delante si quieres filtrar publicidad con panel web y listas de bloqueo.