Pihole on Fedora Coreos via iPXE

Requirments / Necessities

  • DHCP (Juniper SRX)

  • HTTP (Synology)

  • microSD cards (I have 4 or 8GB)

  • (optional) Power-over-Ethernet Switch and PiHats

  • Raspberry Pi 4(s)

Setup

  • IP 172.20.10.11 is the IP for the Synology.

  • IP 172.16.1.11 is the IP for a Raspberry Pi 4.

DHCP

The following Juniper SRX configuration (snipet) is what I use in my network for iPXE:

[edit access address-assignment pool lease-default family inet]
network 172.16.0.0/16;
dhcp-attributes {
    server-identifier 172.16.0.1;
    name-server {
        172.16.1.14;
        172.16.1.13;
        172.16.1.11;
        1.1.1.1;
    }
    router {
        172.16.0.1;
    }
    boot-file http://172.20.10.11/ipxe/fcos.ipxe; (1)
    boot-server 172.20.10.11; (2)
    tftp-server 172.20.10.11; (3)
}
host rpi1 {
    hardware-address dc:a6:32:f0:d4:b3;
    ip-address 172.16.1.11;
}
  1. Set to the location of the PXE bootloader/filename. Also known in ISC DHCPD: filename (Option 67).

  2. Set to the server of the PXE/TFTP server. Otherwise, the client might try to download the boot-file from the SRX.

  3. (Also) Set to the server of the PXE/TFTP server. Also known in ISC DHCPD: next-server (Option 66).

Now when a Raspberry Pi dies I just have to replace the DHCP Reservation and the new Raspberry Pi will boot into my config

HTTP

I use a HTTP Server to speed up the downloading of Fedora CoreOS, but TFTP works too. The benefits of using HTTP is the protocol is much faster than TFTP.

MicroSD Boot

I tried to use the Raspberry Pi 4’s native capability to network boot without a microSD card, but since we are booting a UEFI firmware then the iPXE bootloader, which boots Fedora CoreOS; the complexities are too great to boot everything from network. Using a iPXE image on microSD avoids having to deal with the chainloading issues.

Configuration Files

iPXE

As seen in the boot-file/filename, the fcos.ipxe is a simple script that gets run by the iPXE bootloader. The following code is for the iPXE script.

#!ipxe

iseq ${net0/ip} 172.16.1.10 && set CONFIGURL http://172.20.10.11/coreos/rpi0.ign ||
iseq ${net0/ip} 172.16.1.11 && set CONFIGURL http://172.20.10.11/coreos/rpi1.ign ||
iseq ${net0/ip} 172.16.1.12 && set CONFIGURL http://172.20.10.11/coreos/rpi2.ign ||
iseq ${net0/ip} 172.16.1.13 && set CONFIGURL http://172.20.10.11/coreos/rpi3.ign ||
iseq ${net0/ip} 172.16.1.14 && set CONFIGURL http://172.20.10.11/coreos/rpi4.ign ||
iseq ${net0/ip} 172.16.1.15 && set CONFIGURL http://172.20.10.11/coreos/rpi5.ign ||
iseq ${net0/ip} 172.16.1.16 && set CONFIGURL http://172.20.10.11/coreos/rpi6.ign ||
iseq ${net0/ip} 172.16.1.17 && set CONFIGURL http://172.20.10.11/coreos/rpi7.ign ||

set KERNELFILE kernel-aarch64.gz
set INITRDFILE initramfs.aarch64.img
set ROOTFSFILE rootfs.aarch64.img

set BASEURL http://172.20.10.11/fedora-coreos
set ROOTFSURL ${BASEURL}/${ROOTFSFILE}

set KERNELOPT console=tty1 rw
set COREOSOPT coreos.live.rootfs_url=${ROOTFSURL}
set IGNITNOPT ignition.firstboot ignition.platform.id=metal ignition.config.url=${CONFIGURL}

kernel ${BASEURL}/${KERNELFILE} initrd=main ${COREOSOPT} ${IGNITNOPT} ${KERNELOPT}
initrd --name main ${BASEURL}/${INITRDFILE}
boot

iPXE should have a dhcp ip before running this script. In my environment, the boot will error out at various points before this script is executed.

Butane

variant: fcos
version: 1.4.0
passwd:
  users:
  - name: core
    ssh_authorized_keys:
    - ssh-rsa <SSH_KEY> snelson@example.com
storage:
  directories:
  - overwrite: true
    path: /opt/pihole/etc
  - overwrite: true
    path: /opt/pihole/dnsmasq.d
  files:
  - path: /etc/NetworkManager/conf.d/dns.conf
    contents:
      inline: |
        [main]
        dns=none
    group:
      id: 0
    mode: 420
    user:
      id: 0
  - path: /etc/resolv.conf
    contents:
      inline: |
        nameserver 1.1.1.1
    group:
      id: 0
    mode: 420
    overwrite: true
    user:
      id: 0
  - path: /etc/hostname
    contents:
      inline: rpi1.<BASE_DOMAIN>
    mode: 420
  - path: /opt/pihole/etc/environment
    contents:
      inline: |
        TZ=America/Denver
        WEBPASSWORD=<PASSWORD>
    mode: 420
systemd:
  units:
  - name: coreos-migrate-to-systemd-resolved.service
    enabled: false
    mask: true
  - name: systemd-resolved.service
    enabled: false
    mask: true
  - name: pihole.service
    enabled: true
    contents: |
      [Unit]
      Description=Pi-hole
      After=network-online.target
      Wants=network-online.target

      [Service]
      TimeoutStartSec=0
      ExecStop=/bin/podman stop pihole
      ExecStartPre=mkdir -p /opt/pihole
      ExecStartPre=-/bin/podman kill pihole
      ExecStartPre=-/bin/podman rm pihole
      ExecStartPre=-/bin/podman pull docker.io/pihole/pihole:latest
      ExecStart=/bin/podman run \
          --net=host \
          --name pihole \
          --volume /opt/pihole/etc/:/etc/pihole/:z \
          --volume /opt/pihole/dnsmasq.d/:/etc/dnsmasq.d/:z \
          --env-file /opt/pihole/etc/environment \
          --cap-add=NET_ADMIN \
          docker.io/pihole/pihole:latest

      [Install]
      WantedBy=multi-user.target

The two big pieces for getting Pihole to run on CoreOS is the pihole.service systemd service file and /opt/pihole/etc/environment, which sets important container environment variables like the Time Zone and the WebUI admin password. Within the pihole.service, you could add specific port exposures to limit access as Fedora CoreOS doesn’t currently (CY22Q1) enable the firewall.

For this blog post, the CoreOS Butane configuration has been combined. In reality (now), my butnane configuration are separate butane configs that get compiled into individual igntion configs which get "merged" via the ignition executor in CoreOS on boot. The decomposition is done by function (hostname, network config, etc) with separate butane configs for the container configuration and one config for the SystemD service that starts the container.

Compiling Butane

Please read the official Documentation for how to use Fedora CoreOS and Butane for configuration.

butane -s -d . config.bu -o rpi1.ign

Conclusion

Even though the Raspberry Pi still needs microSD cards to boot, the longevity of the microSD cards should be much longer as only a portion of the card is written to and read from. The bulk of the OS is loaded into and ran from RAM, so if the application needs the bulk of available RAM, this method wouldn’t be suitable.

Using Butane to merge multiple Ignition configs via CoreOS Boot

variant: fcos
version: 1.4.0
ignition:
  config:
    merge:
      - source: http://172.20.10.11/coreos/base/host/users/core.ign
      - source: http://172.20.10.11/coreos/base/host/network/bootstrap.ign
      - source: http://172.20.10.11/coreos/base/host/network/fqdn/hostname-rpi1.ign
      - source: http://172.20.10.11/coreos/base/apps/pihole/pihole-environment.ign
      - source: http://172.20.10.11/coreos/base/apps/pihole/pihole-service.ign