Raspberry Pi mit Pi-Hole und Unbound DNS-Server

Ziel der Übung ist es, einen Raspberry Pi so zu konfigurieren, dass er einerseits DNS-Anfragen mittels Pi-Hole filtert und damit Werbung, Spam und Malware unterbindet und andererseits als rekursiver DNS-Resolver agiert, zu diesem Zweck soll Unbound zum Einsatz kommen.

Inhalt

  1. Betriebssystem
  2. Pi-Hole
  3. Unbound
  4. Pi-Hole
  5. Optionale Schritte
  6. Fazit

Betriebssystem

Als Betriebssytem möchte ich Ubuntu Server 18.04.4 LTS verwenden, da ich damit schon ein wenig Erfahrung habe und diese erweitern möchte.

Installation

Ich lade das 64-Bit-Image herunter, entpacke es mit 7Zip und schreibe es mit Win32DiskImager (Etcher geht auch) auf eine 32GB große SanDisk Extreme microSD-Karte, die gerade herumliegt. Diese schiebe ich in den Raspberry Pi 3 B und verbinde ihn mit Netzwerk und Strom.

Das Betriebssystem ist standardmäßig auf DHCPv4 eingestellt und bekommt somit von meinem Router eine IPv4-Adresse zugewiesen.

Ich benutze Putty, um mich per SSH auf den Raspberry Pi zu verbinden und melde mich mit den Standard-Zugangsdaten an, bei diesem Image sind das ubuntu/ubuntu. Bei Aufforderung ändere ich das Passwort.

Ich aktualisiere das Betriebssystem mit den beiden Befehlen

sudo apt update
sudo apt upgrade

Benutzer ändern (optional)

Ich möchte einen anderen Benutzernamen verwenden und lege dazu einen neuen Benutzer namens newuser an, dabei vergebe ich ein sicheres Passwort und füge ihn zur Gruppe der Sudoers (Administratoren) hinzu:

sudo adduser newuser
sudo usermod -aG sudo newuser

Ich öffne eine neue Session in Putty und melde mich mit dem neuen Benutzer an. Nachdem das klappt, beende ich die erste SSH-Session und lösche dann den alten User ubuntu und sein home-Verzeichnis.

sudo userdel -r ubuntu

Netzwerk

Hostnamen ändern (optional)

Ich ändere dazu den Hostnamen mittels hostnamectl und füge dann noch die Einträge 127.0.0.1 dns-vie und ::1 dns-vie zur Datei /etc/hosts hinzu.

sudo hostnamectl set-hostname dns-vie
sudo nano /etc/hosts
IP-Adressen vergeben

Seit Ubuntu 17.10 wird Netplan verwendet. Das heißt, die unter /etc/netplan/ abgelegte Konfigurationsdatei wird gelesen und für die Netzwerkkonfiguration angewendet. Bei diesem Betriebssystem heißt diese Datei 50-cloud-init.yaml und beinhaltet standardmäßig folgendes:

# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth0:
            dhcp4: true
            optional: true
    version: 2

Ich ändere die Konfiguration entsprechend meiner Wünsche, sie sieht dann in etwa so aus:

# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        eth0:
          dhcp4: no
          dhcp6: no
          accept-ra: true
          addresses:
            - 192.0.2.53/24
            - 2001:db8::53/64
          gateway4: 192.0.2.1
          gateway6: 2001:db8::1
          nameservers:
            addresses:
             - 127.0.0.1
             - ::1
    version: 2

Ich habe die echten IP-Adressen durch welche, die zu Dokumentationszwecken gedacht sind, ersetzt. 192.0.2.1 bzw. 2001:db8::1 wäre also mein Router.

Laut der Info in der Datei überstehen Änderungen darin keinen Reboot. Ich versuche es und die Einstellungen bleiben sehrwohl erhalten. Zur Sicherheit befolge ich aber trotzdem die Anleitung und erstelle die Datei 99-disable-network-config.cfg und befülle sie danach mit dem Text network: {config: disabled}:

sudo nano /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
network: {config: disabled}

Ich Benenne die Konfigurationsdatei von Netplan dann noch um, damit ich zukünftig erkenne, dass das meine Konfiguration ist und wende anschließen die Änderungen an:

sudo mv /etc/netplan/50-cloud-init.yaml /etc/netplan/01-mrconfig.yaml
sudo netplan apply

Um ganz, ganz sicher zu gehen, dass alles korrekt übernommen wurde, schalte ich den Raspberry Pi aus, trenne ihn vom Netzteil, starte ihn dann neu und überprüfe die Konfiguration (ja, ich weiß, ist übertrieben):

sudo poweroff
ip a

Pi-Hole

Pi-Hole ist ein DNS-Server mit integrierter Filterung der Anfragen anhand von Blocklists. Das heißt, dass DNS-Anfragen für Domains, die bekannterweise Werbung, Spam oder Malware verbreiten, garnicht weitergeleitet werden.

Installation

Es sei angemerkt, dass die ubuntu-Version 18.04.4 von Pi-hole eigentlich (noch) nicht unterstützt wird, das sollte aber keine großen Probleme machen. Ich lade also Pi-Hole herunter und starte die Installation, dafür reicht folgender Befehl:

sudo curl -sSL https://install.pi-hole.net | bash

Ich wähle zu Anfang immer die Standardeinstellungen.

Bei der Auswahl der Blocklists wähle ich die letzte (HostsFile) ab, da diese nicht mehr zu existieren scheint.

Die nächsten Dialoge werden alle mit <Ok> beantwortet. Ist die Installation dann fertig, erscheint dieser Dialog mit dem initialen Passwort für die Admin-Weboberfläche.

Jetzt ändere ich noch das Passwort auf ein längeres und sichereres:

sudo pihole -a -p

Für den Moment bin ich mit Pi-Hole fertig, sobald Unbound läuft, muss ich hier noch ein paar Änderungen vornehmen


Unbound

Wie eingangs erwähnt, möchte ich Pi-Hole als Werbeblocker und Unbound als DNS-Server benutzen. Hierzu ein kleiner Exkurs, wie DNS im Prinzip funktioniert und warum ich Unbound verwenden möchte:

Angenommen, man tippt die URL https://blog.resch.cloud in seinen Browser, so fragt dieser das Betriebssystem, ob es die Domain kennt, also weiß, was die IP-Adressen dahinter sind. Kennt das Betriebssystem die Adressen nicht, so fragt es den dort eingestellten DNS-Server (z.B. den Router). Kennt auch dieser die Antwort nicht, fragt er den in ihm eingetragenen Server, z.B. Google (8.8.8.8 bzw. 2001:4860:4860::8888) und so weiter und so fort.

Angenommen, niemand kennt bis dahin die Domain, so endet die Anfrage bei einem der Root-Server. Dieser gibt zur Antwort, wer die Top-Level-Domain .cloud verwaltet. Also wird dann dieser Server befragt, und der weiß, welcher Nameserver .resch.cloud verwaltet und dieser kennt dann die ganze Antwort, also die IP-Adressen von blog.resch.cloud.

Der Haken an dieser Sache ist, dass alle diese Server und deren Besitzer dann bescheid wissen, wer wann von wo welche Adressen aufruft.

Unbound ist ein Stück Software, das direkt die Root-Server befragt und damit alle Zwischenschritte überspringt. Somit wissen nur diejenigen über meine Anfragen bescheid, die auch tatsächlich eine Antwort darauf geben können.

Installation

Mit folgendem Befehl lade und installiere ich Unbound. Falls am Ende der Installation ein Fehler auftritt, liegt das wahrscheinlich an der noch fehlenden Konfiguration, was in Kürze behoben wird.

sudo apt install unbound
Konfiguration

Dann lade ich die Liste der Root-Server herunter und verschiebe sie an einen Ort, wo Unbound sie erwartet (diese Liste sollte alle paar Wochen bis Monate aktualisiert werden, weiter unten automatisiere ich diesen Prozess):

sudo wget -O root.hints https://www.internic.net/domain/named.root
sudo mv root.hints /var/lib/unbound/

Die Liste beinhaltet die IPv4 und IPv6-Adressen der Root-Server, diese Einträge sehen in etwa so aus (Auszug):

; FORMERLY NS.INTERNIC.NET
;
.                        3600000      NS    A.ROOT-SERVERS.NET.
A.ROOT-SERVERS.NET.      3600000      A     198.41.0.4
A.ROOT-SERVERS.NET.      3600000      AAAA  2001:503:ba3e::2:30

Ich erstelle eine Konfigurationsdatei namens pi-hole.conf unter /etc/unbound/unbound.conf.d/ entsprechend dieser Anleitung und passe sie so an, dass Unbound fürs Erste direkt und von überall als DNS-Resolver verwendbar ist (Die letzten vier Zeilen). Die Option do-ip6 bezieht sich lediglich darauf, ob unbound die Root-Server auch per IPv6 kontaktiert, ist also nur sinnvoll, wenn man IPv6-Konnaktivität zum Internet hat. Die Option hat keinen Einfluss darauf, welche Adressfamilien aufgelöst werden!

Die Konfiguration sieht dann in etwa so aus:

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

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

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

    # Use this only when you downloaded the list of primary root servers!
    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.
    # Suggested by the unbound man page to reduce fragmentation reassembly problems
    edns-buffer-size: 1472

    # 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: fc00::/7
    private-address: fe80::/10

    interface: 0.0.0.0
    interface: ::0

    access-control: 0.0.0.0/0 allow
    access-control: ::/0 allow

Ich starte den Dienst. Falls das fehlschlägt, sind in der Konfiguration wahrscheinlich noch Fehler.

sudo service unbound start

Ein kurzer Test auf einer anderen Linux-Maschine zeigt, dass die Namensauflösung funktioniert, am PC funktioniert es jedoch nicht. Wie üblich hat Windows scheinbar ein paar Eigenheiten (Bugs), das stellt aber momentan kein Problem dar.

DNSSEC

DNSSEC ist ein Mittel, um die Echtheit von DNS-Daten zu gewährleisten.

Eine ausführlichere Erklärung abgeben zu können, möchte ich mir nicht anmaßen, daher folgendes Video:

Abschließend teste ich die Validierung durch DNSSEC mit den folgenden Befehlen. Der erste soll den Status „SERVFAIL“ zurückgeben, der zweite „NOERROR„. Das ganze wiederhole ich per IPv6.

dig sigfail.verteiltesysteme.net @127.0.0.1 -p 5353
dig sigok.verteiltesysteme.net @127.0.0.1 -p 5353
dig sigfail.verteiltesysteme.net @::1 -p 5353
dig sigok.verteiltesysteme.net @::1 -p 5353
Abschluss der Konfiguration

Nachdem Unbound nur auf Anfragen von Pi-Hole antworten soll, passe ich die Konfiguration entsprechend an, indem ich die letzten vie Zeilen mit # auskommentiere und Unbound neu starte.

    #interface: 0.0.0.0
    #interface: ::0

    #access-control: 0.0.0.0/0 allow
    #access-control: ::/0 allow
sudo service unbound restart

Pi-Hole

Jetzt muss Pi-Hole noch seine Anfragen an Unbound richten, daher werde ich die Konfiguration jetzt abschließen.

Konfiguration

Ich rufe die Weboberfläche von Pi-Hole unter http://ip/admin auf und melde mich an.

Unter Settings -> DNS deaktiviere ich alle vorgegebenen Anbieter und trage die IP-Adressen und Ports von Unbound ein, also die localhost-Adressen und den Port 5353.

Die Option Use DNSSEC lasse ich deaktiviert, nachdem das Unbound ohnehin macht.

Die Option Use Conditional Forwarding aktiviere ich und trage ein, dass alle Anfragen an die Domain resch.cloud nicht zu Unbound sondern an meinen Router geleitet werden sollen. Nachdem ich die Domain resch.cloud auch für mein internes Netzwerk verwende, stelle ich so sicher, dass auch diese Anfragen richtig aufgelöst werden können.

Leider funktioniert das in der Weboberfläche von Pi-hole nur für IPv4. Außerdem funktioniert Reverse DNS so nur für IPv4-Adressen in einem /24-Subnetz der angegeben Router-Adresse (hier wäre das 192.0.2.0/24).

Use Conditional Forwarding ändert die Datei /etc/dnsmasq.d/01-pihole.conf. Diese passe ich folgendermaßen an, sodass alle PTR-Anfragen für übliche private Adressbereiche an meinen Router weitergegeben werden (hier benutze ich aber echte private Adressen anstatt welcher für Dokumentationszwecke):

interface=eth0
# Diese Zeile wurde von Pi-Hole eingetragen:
server=/resch.cloud/10.1.1.1
# Diese ebenso, ich habe sie aber auf 10.0.0.0/8 erweitert:
server=/10.in-addr.arpa/10.1.1.1
# Diese Zeile wurde von Pi-Hole eingetragen:
server=/use-application-dns.net/
# Diese Zeile entspricht der ersten, jedoch für IPv6:
server=/resch.cloud/fc01:1::1
# Diese Zeilen definieren zwei weitere private IPv4-Adressbereiche:
server=/16.172.in-addr.arpa/10.1.1.1
server=/168.192.in-addr.arpa/10.1.1.1
# Diese Zeilen definieren private IPv6-Bereiche:
server=/c.f.ip6.arpa/fc01:1::1
server=/d.f.ip6.arpa/fc01:1::1
sudo pihole restartdns

Der Raspberry Pi müsste jetzt aus dem gesamten Netzwerk per IPv4 und IPv6 über den Standardport als DNS-Server nutzbar sein und somit habe ich mein primäres Ziel erreicht.


Optionale Schritte

Firewall

Mit folgenden Befehlen erlaube ich SSH, DNS, HTTP und HTTPS auf der Firewall und aktiviere diese anschließend. Alles andere wird automatisch blockiert.

sudo ufw allow ssh
sudo ufw allow 53/udp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

root-hints automatisch aktualisieren

Um die Liste der ROOT-Nameserver (root-hints) automatisch zu aktualisieren, erstelle ich ein Skript namens root-hints-update.sh mit folgendem Inhalt in meinem home-Verzeichnis.

wget -O root.hints https://www.internic.net/domain/named.root
mv root.hints /var/lib/unbound/
service unbound restart

Ich mache es ausführbar und teste es

sudo chmod +x root-hints-update.sh
sudo ./root-hints-update.sh

Jetzt erstelle ich noch einen Crontab-Eintrag (Zeitplan), der das Skript wöchentlich ausführt. Da es mit root-Rechten ausgeführt werden muss, erstelle ich auch den Crontab-Eintrag mit sudo.

sudo crontab -e
0 0 * * 0 /home/username/root-hints-update.sh > /tmp/crontab.log

Apache statt lighttpd einrichten

Nachdem ich Apache schon etwas kenne und auch aus Interesse möchte ich den mit Pi-Hole mitinstallierten Webserver lighttpd durch Apache ersetzen. Nachdem ich dann auch TLS1.3 verwenden möchte und die letzte offiziell von Ubuntu 18.04.4 unterstützte Apache-Version das noch nicht kann, verwende ich ein alternatives Repository von Ondřej Surý. Dadurch wird derzeit die Apache-Version 2.4.41 installiert.

Edit (5.6.2020): Ab Ubuntu 20.04 ist die Verwendung des alternativen Repository nicht mehr nötig.

Ab Apache 2.4.36 und OpenSSL 1.1.1 wird TLS1.3 unterstützt.

sudo service lighttpd stop
sudo add-apt-repository ppa:ondrej/apache2
sudo apt update
sudo apt upgrade
sudo apt install apache2

Das www-Verzeichnis von Apache ist gleich wie das von lighttpd, somit muss ich hier nichts ändern. Die Admin-Weboberfläche von Pi-Hole wird jetzt schon von Apache ausgeliefert, ist mangels PHP aber noch etwas kaputt.

Das Apache-Modul PHP muss nachinstalliert werden. Es muss der installierten PHP-Version entsprechen, daher frage ich diese ab und installiere dann das Modul.

php -v
sudo apt install libapache2-mod-php7.2

Danach müsste die Weboberfläche in Ordnung sein und der alte Webserver lighttpd kann entfernt werden.

sudo apt remove lighttpd

Einige Dinge auf der Weboberfläche funktionieren jetzt wahrscheinlich noch nicht und man erhält die Fehlermeldung „Attempt to write a readonly database„. Das liegt an fehlenden Gruppenzuordnungen, diese erstelle ich mit folgenden Befehlen, dann starte ich Pi-hole und apache neu.

sudo usermod -aG pihole www-data
sudo usermod -aG www-data pihole
sudo pihole restartdns
sudo systemctl restart apache2
TLS-Konfiguration

Nachdem ich eine außerordentliche Abneigung gegen HTTP (ohne S) und durchgestrichene Schlösser habe, richte ich einen Domainnamen im Router ein und erstelle ein passendes TLS-Zertifikat in XCA.

Ich exportiere das Zertifikat als .crt und den privaten Schlüssel als .pem und lege beides unter /etc/ssl/ ab. Dazu benutze ich WinSCP. Um den privaten Schlüssel zu schützen, ändere ich die Dateiberechtigung auf -rw——- (600).

sudo chmod 600 /etc/ssl/dns-vie.resch.cloud.pem

Anschließend muss Apache konfiguriert werden. Dazu muss zuerst das Modul SSL aktiviert werden:

sudo a2enmod ssl

Dann wird die Konfiguration des Virtual Hosts angepasst. In meinem Fall gibt es nur einen, daher benutze ich die Standard-Konfigurationsdatei /etc/apache2/sites-enabled/000-default.conf und passe sie folgendemaßen an (alles auskommentierte habe ich hier entfernt):

<VirtualHost *:80>
  ServerName dns-vie.resch.cloud
  Redirect / https://dns-vie.resch.cloud
</VirtualHost>

<VirtualHost *:443>
        ServerName dns-vie.resch.cloud

        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html

        SSLEngine on
        SSLCertificateFile /etc/ssl/dns-vie.resch.cloud.crt
        SSLCertificateKeyFile /etc/ssl/dns-vie.resch.cloud.pem
        SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Erstens leite ich alle Anfragen, die per HTTP kommen, auf den HTTPS-Port um, zweitens gebe ich die Speicherorte des Zertifikats und des privaten Schlüssels an und drittens deaktiviere ich alle alten und unsicheren Protokolle, sodass nur TLS1.2 und TLS1.3 übrig bleiben. Dann starte ich Apache neu und die ganze Sache ist erledigt.

sudo service apache2 restart

Pi-Hole-Update

Hat man lighttpd NICHT durch einen anderen Webserver ersetzt, kann man Pi-Hole mit folgendem Befehl aktualisieren.

sudo pihole -up

In dem hier beschriebenen Fall geht das nicht, weil das Update-Programm lighttpd natürlich nicht finden kann. Daher muss man vor einem Update die Datei /etc/pihole/setupVars.conf bearbeiten und die entsprechende Zeilen fogendermaßen anpassen.

INSTALL_WEB_SERVER=false
LIGHTTPD_ENABLED=false

Auch die Datei /etc/dnsmasq.d/01-pihole.conf muss danach eventuell wie bei der Installation neu editiert werden.


Fazit

Mein Ziel, einen Pi mit Pi-Hole und Unbound aufzusetzen habe ich erreicht. Zusätzlich habe ich in den rund 14 Stunden, die ich brauchte, um mir diese Vorgehensweise zu erarbeiten und zu dokumentieren sehr viel neues Wissen aufgebaut. Eigentlich wäre das ganze innerhalb von ca. 3 Stunden irgendwie funktionsfähig gewesen aber von ordentlich, dokumentiert, lehrreich und nachvollziehbar hätte man dann nicht sprechen können.

Daher habe ich zuerst die einzelnen Komponenten installiert und konfiguriert dafür jeweils immer wieder mit einem frischen Image begonnen.

Anfangs habe ich damit begonnen, zuerst Unbound und erst danach Pi-Hole zu installieren, was mehrfach gescheitert ist, da es während der Installation von Pi-Hole immer wieder Probleme mit der Namensauflösung gab und so Installationsdateien nicht heruntergeladen worden sind.

So konnte ich die Zusammenhänge besser nachvollziehen und verstehen, unsaubere konfigurationen vermeiden und dafür sorgen, dass jeder mit der ausreichenden Motivation anhand dieses Artikels dasselbe Ziel erreichen kann.


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.