Para seguir el proceso de migración de mi Home Lab desde una pequeña Raspberry Pi 4 a un OptiPlex 7050 ejecutando Proxmox ahora le toca el turno a Pi-hole, un sumidero de DNS que protege a los dispositivos de contenido no deseado, sin instalar ningún programa del lado del cliente.

En esta ocasion, en lugar de realizar la instalación de Pi-hole en Docker, se utilizará un contenedor ligero LXC de Proxmox con TurnKey Core. El procedimiento para crear este LXC es el mismo que usé para ejecutar Docker en Proxmox LXC.

Características del LXC

Este sumidero de DNS con Pi-hole se ejecutará sobre un LXC con las siguientes características:

  • LXC no privilegiado, sin nesting y sin keyctl
  • CT ID 304
  • Password + SSH key file
  • Disco de 8GB
  • CPU con 1 core
  • 512MB de memoria
  • IP fija 192.168.1.81
pct create 304 local:vztmpl/debian-12-turnkey-core_18.0-1_amd64.tar.gz \
  --ostype debian --arch amd64 \
  --hostname pihole1 --unprivileged 1 --password P@ssw0rd --ssh-public-keys /root/id_edcsa.pub \
  --storage local-lvm --rootfs volume=local-lvm:8 \
  --cores 1 \
  --memory 512 --swap 512 \
  --net0 name=eth0,bridge=vmbr0,firewall=1,ip=192.168.1.81/24,gw=192.168.1.1 \
  --start false

A continuación se pone en marcha el LXC desde la CLI de Proxmox y se procede a configurarlo de la misma manera que ya expliqué en el artículo sobre LXC y Docker:

pct start 304

Acceder al LXC

Se puede acceder al LXC utilizando la opción Console de la GUI o, mucho mejor, mediante SSH gracias a la configuración de las claves públicas que se hizo en el mismo:

ssh root@192.168.1.81

Una vez dentro del LXC, hay que:

  • completar el asistente de instalación
  • reiniciar el LXC (sobretodo si hay una actualización del kernel)
  • reconfigurar la zona horaria
  • actualizar los paquetes
  • desinstalar el paquete etckeeper
  • reiniciar el LXC

Instalación de Pi-hole

Antes de comenzar la instalación es necesario deshabilitar el DNSStubListener de forma manual para evitar errores al reconfigurar el servicio systemd-resolved:

sed -r -i.orig 's/#?DNSStubListener=yes/DNSStubListener=no/g' /etc/systemd/resolved.conf
systemctl restart systemd-resolved.service

En TurnKey Core 18.0 (basado en Debian 12) no existe el fichero anterior y el servicio está masked.

La instalación de Pi-hole en una distribución basada en Debian es muy sencilla usando el script basic-install.sh:

cd ~
wget -O basic-install.sh https://install.pi-hole.net
bash basic-install.sh

Si se cumplen los requisitos para instalar Pi-hole, se ejecutará el asistente Pi-hole Automated Installer mediante el cual se proporcionará la información necesaria para la instalación:

  • Static IP Needed: Continue
  • Upstream DNS Provider: Quad9 (filtered, DNSSEC)
  • Blocklist: Yes (para incluir la lista StevenBlack’s Unified Host List)
  • Admin Web Interface: Yes
  • Web Server: Yes
  • Enable Logging: Yes
  • Privacy Mode: Show everything

Al finalizar la instalación, si no hay ningún problema, aparecerá una pantalla mostrando el password generado de forma aleatoria y la URL de la interfaz web de administración http://192.168.1.81/admin.

Nota: Se puede utilizar el comando pihole -a -p para cambiar la constraseña de la interfaz web y el comando pihole -up para actualizar la versión.

Forwarding vs Recursive

En una instalación normal de Pi-hole, se utiliza una configuración Forwarding para reenviar al siguiente proveedor de DNS aquellas peticiones que no pueda resolver de manera local consultando la lista de bloqueos.

Esta configuración, aunque es funcional y sencilla, tiene algunos problemas de privacidad y seguridad:

  • Privacidad: El proveedor de DNS podría tener un listado completo de las páginas web que visitamos si guarda información sobre los dominios que pide un determinado cliente.
  • Seguridad: Podríamos ser víctimas de un ataque de DNS Spoofing que nos redirija a una página fake si el proveedor DNS altera las respuestas.

Por este motivo es recomendable utilizar una configuración Recursive mediante unbound donde, en lugar de preguntar a un proveedor de DNS, se consulta la IP directamente al servidor autoritativo del dominio que se está buscando.

Nota: Otra opción, sería utilizar un DNS-over-HTTPS mediante cloudflared para evitar las peticiones en texto plano tal como hace mi compañero Roberto P. Rubio.

Instalación de unbound

La instalación de unbound se realiza de forma muy sencilla desde los repositorios usando el comando apt:

apt install unbound -y

Nota: Inicialmente puede que aparezca el error Failed to start Unbound DNS server ya que el puerto 53 está siendo usado por Pi-hole.

Configuración

Es necesario crear un fichero de configuración de unbound para indicar, entre otras cosas, el puerto 5353 en el que tiene que escuchar para que no haya conflictos:

nano /etc/unbound/unbound.conf.d/pi-hole.conf

Escribir el siguiente contenido y grabar el fichero de configuración:

server:
    # If no logfile is specified, syslog is used
    # logfile: "/var/log/unbound/unbound.log"
    verbosity: 0

    interface: 127.0.0.1
    port: 5353
    do-ip4: yes
    do-udp: yes
    do-tcp: yes

    # May be set to yes if you have IPv6 connectivity
    do-ip6: no

    # You want to leave this to no unless you have *native* IPv6. With 6to4 and
    # Terredo tunnels your web browser should favor IPv4 for the same reasons
    prefer-ip6: no

    # Use this only when you downloaded the list of primary root servers!
    # If you use the default dns-root-data package, unbound will find it automatically
    #root-hints: "/var/lib/unbound/root.hints"

    # Trust glue only if it is within the server's authority
    harden-glue: yes

    # Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
    harden-dnssec-stripped: yes

    # Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
    # see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378 for further details
    use-caps-for-id: no

    # Reduce EDNS reassembly buffer size.
    # IP fragmentation is unreliable on the Internet today, and can cause
    # transmission failures when large DNS messages are sent via UDP. Even
    # when fragmentation does work, it may not be secure; it is theoretically
    # possible to spoof parts of a fragmented DNS message, without easy
    # detection at the receiving end. Recently, there was an excellent study
    # >>> Defragmenting DNS - Determining the optimal maximum UDP response size for DNS <<<
    # by Axel Koolhaas, and Tjeerd Slokker (https://indico.dns-oarc.net/event/36/contributions/776/)
    # in collaboration with NLnet Labs explored DNS using real world data from the
    # the RIPE Atlas probes and the researchers suggested different values for
    # IPv4 and IPv6 and in different scenarios. They advise that servers should
    # be configured to limit DNS messages sent over UDP to a size that will not
    # trigger fragmentation on typical network links. DNS servers can switch
    # from UDP to TCP when a DNS response is too big to fit in this limited
    # buffer size. This value has also been suggested in DNS Flag Day 2020.
    edns-buffer-size: 1232

    # Perform prefetching of close to expired message cache entries
    # This only applies to domains that have been frequently queried
    prefetch: yes

    # One thread should be sufficient, can be increased on beefy machines.
    # In reality for most users running on small networks or on a single machine, it should
    # be unnecessary to seek performance enhancement by increasing num-threads above 1.
    num-threads: 1

    # Ensure kernel buffer is large enough to not lose messages in traffic spikes
    so-rcvbuf: 1m

    # Ensure privacy of local IP ranges
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8
    private-address: fd00::/8
    private-address: fe80::/10

También es recomendable añadir la siguiente línea al fichero /etc/dnsmasq.d/99-edns.conf para limitar el tamaño de paquete que utilizará Pi-hole FTL tal como se indicó en el DNS Flag Day 2020:

edns-packet-max=1232

A continuación se reinicia el servicio con la nueva configuración y, si todo está bien, debería iniciar correctamente:

systemctl restart unbound.service 
systemctl status unbound.service

Comprobación de resolución

Para comprobar que unbound resuelve nombres correctamente, se puede ejecutar el comando dig indicando la dirección IP y puerto donde escucha:

dig pi-hole.net @127.0.0.1 -p 5353

Si funciona correctamente, se debería obtener un registro de tipo A tal como se muestra a continuación:

; <<>> DiG 9.16.44-Debian <<>> pi-hole.net @127.0.0.1 -p 5353
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54721
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;pi-hole.net.                   IN      A

;; ANSWER SECTION:
pi-hole.net.            300     IN      A       3.18.136.52

;; Query time: 71 msec
;; SERVER: 127.0.0.1#5353(127.0.0.1)
;; WHEN: Sat Nov 18 21:22:21 CET 2023
;; MSG SIZE  rcvd: 56

También se puede realizar una prueba usando DNSSEC para resolver el dominio dnssec.works:

dig dnssec.works +dnssec +multi @127.0.0.1 -p 5353
; <<>> DiG 9.16.44-Debian <<>> dnssec.works +dnssec +multi @127.0.0.1 -p 5353
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 52167
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;dnssec.works.          IN A

;; ANSWER SECTION:
dnssec.works.           3514 IN A 5.45.107.88
dnssec.works.           3514 IN RRSIG A 8 2 3600 (
                                20231211004428 20231110235251 63306 dnssec.works.
                                4AWfdRz9vpEsFEyvy11sh+wOBaTGFPUBDjND/RUu1Er4
                                +P0Pua9ieH2QAu/3Q9XkU/q9fL6KqXHMzP4/6MjKkXpb
                                SFyliuk6wHVuKsC5Ac54IN/bf85TET4cotY+Nufjh0ww
                                ucOgVUh7sJVbQjjmpE0cryyipWU/2mib8Nbk0hp9fx0U
                                d/0H52QxiR24yRvtuXZbaQTZvSzwPc+vIozsqegBOpQW
                                3dbQQCej8Dn808E9L6mZPmajUUdN+GXPyfZ3 )

;; Query time: 0 msec
;; SERVER: 127.0.0.1#5353(127.0.0.1)
;; WHEN: Sat Nov 18 21:27:17 CET 2023
;; MSG SIZE  rcvd: 293

Uso en Pi-hole

Para hacer que Pi-hole utilice unbound en lugar de los servidores indicados durante la instalación, hay que hacer lo siguiente:

  • Iniciar sesió en la interfaz web
  • Acceder a la sección Settings
  • Acceder a la pestaña DNS
  • Desmarcar los Upstream DNS Servers de Quad9
  • Añadir uno de tipo custom: 127.0.0.1#5353
  • Grabar la configuración

Nota: Con la configuración de unbound indicada anteriormente, los nombres que apunten a direcciones privadas según el RFC1918 (por ejemplo 192.168.1.180) no se resuelven por motivos de seguridad.

Si este es nuestro caso, se tiene que agregar la línea private-domain con el nombre del dominio que contiene direcciones privadas al fichero de configuración de unbound.

    # Resolver mi dominio
    private-domain: midominio.com

Servidor secundario

Para evitar interrupciones en la resolución de nombres si el servicio Pi-hole no está disponible (por ejemplo cuando se actualiza o si tiene algún problema) es recomendable tener un segundo servidor.

Aunque lo ideal sería tenerlo en una máquina física diferente, en este caso se aprovechará la flexibilidad de Proxmox para clonar el contenedor LXC en su estado actual.

Antes de ponerlo en marcha habrá que cambiar el nombre y la dirección IP del mismo para evitar conflictos:

  • Network → IP Address: 192.168.1.82/24
  • DNS → Hostname: pihole22

Nota: Al clonar el primer contenedor LXC, éste tendrá las mismas claves SSH del servidor lo cual provoca confusión a la hora de conectarse (This host key is known by the following other names/addresses).

Para solucionarlo hay que conectarse a la Console del LXC en Proxmox para borrar las claves actuales rm /etc/ssh/ssh_host_* y reconfigurar el servidor SSH dpkg-reconfigure openssh-server.

Referencias

Historial de cambios

  • 2023-11-18: Documento inicial
  • 2024-06-22: Revisión y creación de LXC mediante CLI