Zum Hauptinhalt springen

Pi-hole auf Raspberry Pi mit Ansible und Quadlets

·1049 Wörter·5 min
Inhaltsverzeichnis
Raspberry Pi Homelab - Dieser Artikel ist Teil einer Serie.
Teil 2: Dieser Artikel

Dieser Artikel zeigt, wie du Pi-hole als rootless Container mit Podman Quadlets auf dem Raspberry Pi Zero 2 W deployst. Die Ansible-Rolle head1328.pihole übernimmt dabei die komplette Konfiguration inkl. Caddy Reverse Proxy, automatischen Container-Updates und iptables Port-Redirection.

Voraussetzung: ein funktionierendes Podman-Setup wie im ersten Artikel der Serie beschrieben.

Architektur
#

Web-Traffic:

flowchart TB
    A[Browser] --> B["https://pi-hole-primary.fritz.box"]
    B --> C["iptables REDIRECT
443 → 8443"] C --> D["Caddy
internes TLS, Port 8443"] D --> E["http://pihole:80
Podman Network"] E --> F[Pi-hole Admin UI] A ~~~ F

DNS-Traffic:

flowchart LR
    A[DNS-Anfragen] --> B["iptables REDIRECT
53 → 5353"] B --> C["Pi-hole Container
Port 5353"]

Da rootless Podman in der Standardkonfiguration keine privilegierten Ports (< 1024) binden kann, übernimmt iptables die Weiterleitung von Port 53 und 443 auf die Container-Ports 5353 und 8443.

Was ist ein Quadlet?
#

Podman 5 bringt native Quadlet-Unterstützung mit. Quadlets sind systemd-Unit-Dateien, die Container deklarativ beschreiben. Der podman-system-generator wandelt diese beim systemd-Reload automatisch in lauffähige Services um.

Vorteile gegenüber podman generate systemd:

  • Deklarativ statt imperativ
  • Keine generierten Unit-Dateien mehr
  • Native systemd-Integration
  • Automatische Updates via AutoUpdate=registry

Ansible-Projekt erweitern
#

Das komplette Projekt findest du hier:

Dieses Tutorial basiert auf der Version 1.0.0 .

requirements.yml ergänzen
#

---
collections:
  - name: devsec.hardening
    version: ">=10.4.0,<10.5"
    type: galaxy
  - name: containers.podman
    version: ">=1.18.0,<1.19"
    type: galaxy
  - name: community.general           # NEU
    version: ">=10.0.0,<11.0"         # NEU
    type: galaxy                      # NEU

roles:
  - name: head1328.podman
    version: "v0.1.0"
  - name: head1328.pihole             # NEU
    version: "v0.1.0"                 # NEU
  - name: escalate.swap
    version: "v2.1.0"

Die community.general Collection wird für die iptables-Persistierung benötigt.

ansible-galaxy install -r requirements.yml

Inventory erweitern: hosts.yml
#

---
all:
  vars:
    ansible_python_interpreter: /usr/bin/python3
  children:
    podmen:
      children:                                       # NEU
        piholes:                                      # NEU
          hosts:                                      # NEU
            pi-hole-primary:                          # NEU
              ansible_host: pi-hole-primary.fritz.box # NEU

Die piholes-Gruppe ist eine Untergruppe von podmen, sodass beide Rollen angewendet werden.

Group Vars: piholes.yml
#

---
pihole_image: "docker.io/pihole/pihole:latest"
pihole_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  ...
pihole_timezone: "Europe/Berlin"
pihole_dns_upstreams: "5.1.66.255;185.150.99.255"

pihole_fqdn: pi-hole-primary.fritz.box
pihole_in_interface: eth0
pihole_dns_port: 5353
pihole_https_port: 8443

pihole_autoupdate_enabled: true
pihole_autoupdate_time: "03:00"
pihole_autoupdate_random_delay: 900

Das Pi-hole-Passwort verschlüsseln:

# interactive mode (empfohlen)
ansible-vault encrypt_string

# Alternative
ansible-vault encrypt_string 'dein-pi-hole-passwort' --name pihole_password

DNS Upstreams
#

Im Beispiel werden die Freifunk München (FFMUC) Resolver als DNS-Upstream verwendet, datenschutzfreundliche, nicht-kommerzielle DNS-Server der Freifunk-Community. Auch andere Freifunk-Communities in verschiedenen Städten und Ballungsräumen bieten ähnliche Dienste an. Große kommerzielle Anbieter werden hier bewusst nicht genannt.

Playbook erweitern
#

play_pihole.yml
#

---
- name: Deploy Pi-hole
  hosts: piholes
  become: true

  pre_tasks:
    - name: Disable avahi-daemon for Pi-hole
      block:
        - name: Stop avahi-daemon service
          ansible.builtin.systemd:
            name: avahi-daemon
            state: stopped
            enabled: false

    - name: Assert podman binary is available
      ansible.builtin.command: podman --version
      register: podman_version
      changed_when: false
      failed_when: podman_version.rc != 0

  roles:
    - role: head1328.pihole

Der avahi-daemon wird deaktiviert, da er Port 5353 belegt. Auf einem dedizierten Pi-hole Host wird mDNS nicht benötigt, es gibt keine Drucker, AirPlay-Geräte oder andere Dienste die per Zero-Configuration entdeckt werden müssten. Die lokale Namensauflösung übernimmt im FritzBox-Netzwerk ohnehin die FritzBox über .fritz.box Domains.

In play.yml importieren
#

Am Ende der play.yml importierst du das Pi-hole Playbook:

- name: Import Pi-hole playbook
  import_playbook: play_pihole.yml

Was die head1328.pihole Rolle macht
#

SchrittBeschreibung
Quadlet-Verzeichnis~/.config/containers/systemd für rootless Quadlets
Podman NetworkInternes Netzwerk internal für Container-Kommunikation
Pi-hole QuadletContainer-Definition mit DNS-Port und Volumes
Caddy QuadletReverse Proxy mit internem TLS
CaddyfileTemplate für HTTPS-Konfiguration
systemd ServicesQuadlets als User-Services starten
Auto-Update Timerpodman-auto-update.timer konfigurieren
iptablesPort-Redirection 53→5353, 443→8443 (IPv4/IPv6)

Das Pi-hole Quadlet
#

Die Rolle erstellt ein Quadlet unter ~/.config/containers/systemd/pihole.container:

[Container]
ContainerName=pihole
Image=docker.io/pihole/pihole:latest
Network=internal
PublishPort=5353:53/tcp
PublishPort=5353:53/udp
Environment=TZ=Europe/Berlin
Environment=FTLCONF_dns_upstreams=5.1.66.255;185.150.99.255
Environment=FTLCONF_dns_listeningMode=all
Environment=FTLCONF_webserver_api_password=...
AddCapability=SYS_NICE
Volume=pihole-etc-pihole:/etc/pihole:Z
Volume=pihole-etc-dnsmasq.d:/etc/dnsmasq.d:Z
AutoUpdate=registry

[Install]
WantedBy=default.target

Das Caddy Quadlet
#

Caddy übernimmt TLS-Terminierung mit einem internen Zertifikat ~/.config/containers/systemd/caddy.container:

[Container]
ContainerName=caddy
Image=docker.io/caddy:latest
Network=internal
PublishPort=8443:443/tcp
Volume=/etc/caddy/Caddyfile:/etc/caddy/Caddyfile:ro,Z
Volume=caddy-data:/data:Z
Volume=caddy-config:/config:Z
AutoUpdate=registry

[Install]
WantedBy=default.target

Das Caddyfile
#

Die config (/etc/caddy/Caddyfile) wird in den Container ebenfalls nach /etc/caddy/Caddyfile gemounted:

sudo -u podman bash -c 'cd "$HOME" && podman exec -it caddy sh -c "cat /etc/caddy/Caddyfile"'
pi-hole-primary.fritz.box {
    tls internal
    encode gzip

    @root path /
    redir @root /admin 302

    reverse_proxy pihole:80
}

Playbook ausführen
#

ansible-playbook play.yml

Nach dem Playbook-Lauf ist Pi-hole unter https://pi-hole-primary.fritz.box erreichbar. Damit alle Geräte im Heimnetz Pi-hole nutzen, musst du die IP-Adresse des Pi-hole in der FritzBox als lokalen DNS-Server eintragen.

TLS-Zertifikat vertrauen
#

Caddy verwendet eine interne Certificate Authority. Um Browser-Warnungen zu vermeiden, musst du das Root-Zertifikat einmalig exportieren und auf den Client-Geräten als vertrauenswürdig markieren.

Zertifikat exportieren
#

ssh ansible@pi-hole-primary.fritz.box
sudo -u podman bash -c 'cd "$HOME" && podman cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/caddy-root-ca.crt && chmod a+rw /tmp/caddy-root-ca.crt'

Auf macOS vertrauen
#

scp ansible@pi-hole-primary.fritz.box:/tmp/caddy-root-ca.crt .
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain caddy-root-ca.crt

Installation verifizieren
#

Container-Status prüfen
#

ssh ansible@pi-hole-primary.fritz.box
sudo -u podman bash -c 'cd "$HOME" && podman ps'

Erwartete Ausgabe:

CONTAINER ID  IMAGE                           STATUS         NAMES
abc123        docker.io/pihole/pihole:latest  Up 10 minutes  pihole
def456        docker.io/caddy:latest          Up 10 minutes  caddy

systemd Services prüfen
#

sudo -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user status pihole.service'
sudo -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user status caddy.service'

iptables-Regeln prüfen
#

sudo iptables -t nat -L PREROUTING -n -v
sudo ip6tables -t nat -L PREROUTING -n -v

Automatische Updates
#

Die Rolle konfiguriert podman-auto-update für automatische Container-Updates.

Timer-Status prüfen
#

sudo -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user status podman-auto-update.timer'
sudo -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user list-timers'

Manuelles Update
#

# Dry-run
sudo -u podman bash -c 'cd "$HOME" && podman auto-update --dry-run'

# Update durchführen
sudo -u podman bash -c 'cd "$HOME" && podman auto-update'

Update-Zeitpunkt anpassen
#

In host_vars/pi-hole-primary.yml:

pihole_autoupdate_time: "03:00"
pihole_autoupdate_random_delay: 900

Der Random-Delay verhindert, dass mehrere Pi-holes gleichzeitig updaten, wichtig für Hochverfügbarkeit bei Primary/Secondary-Setups.

Fazit
#

Mit der head1328.pihole Ansible-Rolle läuft Pi-hole als rootless Container auf dem Raspberry Pi:

  • Quadlets für deklarative Container-Definition
  • Caddy als Reverse Proxy mit internem TLS
  • iptables für Port-Redirection ohne privilegierte Ports
  • Automatische Updates via podman-auto-update Timer
  • Freifunk München DNS als datenschutzfreundlicher Upstream

Im nächsten Artikel konfigurieren wir ein Primary/Secondary Pi-hole Setup mit FritzBox-Integration und WireGuard VPN.

Verwendete Projekte
#

Eigene Rollen und Playbooks
#


Hat dir das hier geholfen?
Unterstütze gerne via:

Raspberry Pi Homelab - Dieser Artikel ist Teil einer Serie.
Teil 2: Dieser Artikel