Looking for feedback on HA pihole setup with unbound + keepalived + gravitysync

Moderator edit:
This post contains scripts, or links to scripts, that are untested by the Pi-hole team. We encourage users to be creative, but we are unable to support any potential problems caused by running these scripts

Hello everyone,

this is my first post here and I hope I chose the right category. I wasn't sure if the topic is more suitable for general/customizing pihole or help/community or maybe somewhere completely different.

I've read a lot on raspberry pis lately and was looking for a nice project that would keep me occupied for some time, so I decided I wanted to set up a dual pihole HA setup. After reading a lot on it (also in this forum), I decided to go with two raspi zeros with USB2LAN adapters directly connected to my AVM Fritzbox 7590 and to use pihole, unbound, keepalived and gravitysync by @vmstan.

I know that it may be a little overkill, however, I needed a project to spend some time on...

I would like to show you my final setup with the instructions I wrote for myself and would really appreciate any kind of feedback, suggestions for improvement or security/performance concerns, as I'm really a beginner when it comes to network administration, DNS and linux.

Maybe it can even help another beginner to start their project.

1. Raspberry Pi OS: Installation and configuration (same for both raspis)

  • Write Raspberry Pi OS Lite (bullseye) image on SD-card using Raspberry Pi Imager or any other suitable tool
  • Create ssh file on the SD-card to enable ssh access to the raspi
  • Insert SD-card into raspi and boot up
  • Check IP adress in the router and assign a static IP
  • ssh into the raspi via Windows Powershell or putty using ssh pi@xxx.xxx.xxx.xxx where xxx.xxx.xxx.xxx is the ip adress of the raspi
  • Open config with sudo raspi-config to change hostname, password, timezone and expand the filesystem. Reboot.
  • Update packages with sudo apt update && sudo apt upgrade

2. Pihole: installation and configuration (same for both raspis)
Command from the official pihole documentation GitHub - pi-hole/pi-hole: A black hole for Internet advertisements

  • curl -sSL https://install.pi-hole.net | bash
  • Use standard values during installation
  • Log into the pihole web userinterface with http://xxx.xxx.xxx.xxx/admin and go to Groupmanagement -> Adlists and add adlists (good lists for example under https://firebog.net/). You only need to do this for your primary raspi/pihole, because we will sync the lists with the secondary raspi/pihole later on.

3. Unbound: installation and configuration (same for both raspis)
Follow the official installation instructions from the pihole documentation https://docs.pi-hole.net/guides/dns/unbound/

  • Install unbound with sudo apt install unbound
  • Open unbound config with sudo nano /etc/unbound/unbound.conf.d/pi-hole.conf and insert config from the documentation
  • If you use Raspberry Pi OS bullseye you need to open sudo nano /etc/resolvconf.conf and comment out the line starting with unbound_conf and delete the resolvconf_resolver.conf with sudo rm /etc/unbound/unbound.conf.d/resolvconf_resolvers.conf. Further information on this topic can be found here: https://discourse.pi-hole.net/t/warning-raspbian-october-2021-release-bullseye-unbound
  • Restart unbound with new config using sudo systemctl restart unbound
  • Check that unbound works correctly with dig pi-hole.net @127.0.0.1 -p 5335, dig sigfail.verteiltesysteme.net @127.0.0.1 -p 5335 and dig sigok.verteiltesysteme.net @127.0.0.1 -p 5335 which should return NOERROR, SERVFAIL and NOERROR respectively
  • Log into the pihole web userinterface with http://xxx.xxx.xxx.xxx/admin and go to Settings -> DNS to unselect the standard DNS servers and insert 127.0.0.1#5335 in Custom DNS 1 (IPv4)

4. gravitysync: Installation and configuration
Instructions from official installation instructions GitHub - vmstan/gravity-sync: An easy way to synchronize the blocklist and local DNS configurations of multiple Pi-hole 5.x instances.

For the primary raspi/pihole:

  • Install gravitysync using export GS_INSTALL=primary && curl -sSL https://gravity.vmstan.com | bash

For the secondary raspi/pihole:

  • Install gravitysync using export GS_INSTALL=secondary && curl -sSL https://gravity.vmstan.com | bash and when asked enter password of secondary raspi, IP-adress of primary raspi, username of primary raspi and password of primary raspi
  • Go to gravitsync folder with cd ./gravity-sync and check if gravitysync is working correctly with ./gravity-sync.sh compare
  • Pull databases from primary raspi/pihole with ./gravity-sync.sh pull
  • Activate automatic sync with ./gravity-sync.sh automate and define the synchronization frequency when asked

5. postfix: Installation and configuration (same for both raspis)
In this setup postfix is used with a gmail account to allow keepalived (see 6.) to send mails if one of the raspis/piholes goes down and the other one takes over. There are probably a lot of other option to do this.

  • Install postfix and some sasl package with sudo apt install postfix libsasl2-modules and select "Internet Site" when asked during installation
  • Open postfix config with sudo nano /etc/postfix/main.cf and change relayhost = to relayhost = [smtp.gmail.com]:587 and inet_interfaces = all to inet_interfaces = loopback-only
  • Add the following lines at the bottom of the config

smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt

  • Create the file containing the login data for gmail with sudo nano /etc/postfix/sasl_passwd and insert

[smtp.gmail.com]:587 USERNAME@gmail.com:APPPASSWORD

where USERNAME is the gmail username and APPPASSWORD is the app password you can create in the gmail web interface under account settings

  • Create a .db file out of the login data file, that postfix needs for some reason with sudo postmap /etc/postfix/sasl_passwd
  • Secure the files by restricting access as the paswords are written in the files in plain text using sudo chmod 0600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
  • Restart postfix service with new config using sudo systemctl restart postfix

6. keepalived: Installation and configuration
On primary raspi/pihole:

  • Install keepalived with sudo apt install keepalived
  • Open config with sudo nano /etc/keepalived/keepalived.conf and insert
global_defs {
  router_id HOSTNAME                #HOSTNAME: hostname of primary pihole
  notification_email {
      USERNAME@gmail.com			#USERNAME: gmail-username
  }
  smtp_server localhost
  smtp_connect_timeout 30
}

vrrp_track_process track_pihole {
  process pihole-FTL
  weight 50
}

vrrp_instance pihole1 {
   state MASTER
   interface eth0
   virtual_router_id 51
   priority 100
   advert_int 1
   smtp_alert

   unicast_src_ip PRIMARY-PIHOLE-IPADRESS	#PRIMARY-PIHOLE-IPADRESS: xxx.xxx.xxx.xxx
   unicast_peer {
       SECONDARY-PIHOLE-IPADRESS		#SECONDARY-PIHOLE-IPADRESS: xxx.xxx.xxx.xxx
   }
   authentication {
       auth_type PASS
       auth_pass 8-CHARACTER-PASSWORD		#8-CHARACTER-PASSWORD: alphanumerical password with max. 8 chars
   }
   virtual_ipaddress {
      VIRTUAL-IPADRESS				#VIRTUAL-IPADRESS: xxx.xxx.xxx.xxx/24
   }
   track_process {
      track_pihole
   }
}
  • Enable autostart of keepalived service with sudo systemctl enable --now keepalived and restart keepalived with sudo systemctl restart keepalived

On secondary raspi/pihole:

  • Install keepalived with sudo apt install keepalived
  • Open config with sudo nano /etc/keepalived/keepalived.conf and insert
global_defs {
  router_id HOSTNAME				#HOSTNAME: hostname of primary pihole
  notification_email {
      USERNAME@gmail.com			#USERNAME: gmail-username
  }
  smtp_server localhost
  smtp_connect_timeout 30
}

vrrp_track_process track_pihole {
  process pihole-FTL
  weight 50
}

vrrp_instance pihole2 {
   state BACKUP
   interface eth0
   virtual_router_id 51
   priority 90
   advert_int 1
   smtp_alert

   unicast_src_ip Secondary-PIHOLE-IPADRESS	#SECONDARY-PIHOLE-IPADRESS: xxx.xxx.xxx.xxx
   unicast_peer {
       PRIMARY-PIHOLE-IPADRESS			#PRIMARY-PIHOLE-IPADRESS: xxx.xxx.xxx.xxx
   }
   authentication {
       auth_type PASS
       auth_pass 8-CHARACTER-PASSWORD		#8-CHARACTER-PASSWORD: alphanumerical password with max. 8 chars
   }
   virtual_ipaddress {
      VIRTUAL-IPADRESS				#VIRTUAL-IPADRESS: xxx.xxx.xxx.xxx/24
   }
   track_process {
      track_pihole
   }
}
  • Enable autostart of keepalived service with sudo systemctl enable --now keepalived and restart keepalived with sudo systemctl restart keepalived

That's it! Just define the virtual IP-adress as DNS server in your router. Pihole, unbound and gravitysync work fine so far. But keepalived only switches to the secondary pihole if the primary pihole looses network connection or powers of. The vrrp_track_process is not working, unfortunately. If I stop the pihole DNS service on the primary pihole, keepalived will not switch to the secondary pihole. I'm still working on that. Maybe someone here has an idea how to implement this functionality.

Thanks in advance for reading all of this and any feedback you can offer.

1 Like

If you still have some time left, we are always looking for users contributing code, fixing bugs or improving documentation :wink:

thanks floz for sharing, I use a similar configuration I also use unbound + gravity-sync and currently I have problems my primary pihole freezes at 15 hours UTC, I see that you implemented keepalived I don't know what it is used for?, thanks for sharing your configuration is Very helpful, I was looking for this configuration for a long time and it is hardly documented, I use this configuration for 400 clients with 30000 queries

I share a link to configure gravity-sync, I don't know if it helps

Thanks for the offer. Will definitely think about it. Sounds interesting.

Thanks for the link. I will have a look at it. Maybe there's something interesting in it.

I use keepalived for high availability. So there will be a virtual IP adress that will point to the primary pihole as long as it's working correctly. So all DNS requests will got to the primary pihole. But when it goes down, keepalived will switch the virtual IP adress to the secondary pihole. This has the advantage for me, that I have all my pihole statistics in one place on the primary pihole. But keepalived can also do load balancing. So maybe that's also interesting for you to make your network more efficient.

ok thank you very much, I will start to review and implement some things that I need and then I will be commenting to you in case I have problems

I have implemented Keepalived and it is great. Thanks for your contribution. I would like to ask a question. This can be implemented for IPv6. I see that only the virtual IP is only for IPV4.

I only used IPv4 but you can use keepalived also with IPv6 in the same way. However, you can not use both IPv4 and IPv6 addresses as virtual_ipaddresses in a single vrrp_instance. If you want to use both, IPv4 and IPv6, you could define a vrrp_sync_group with two vrrp_instance blocks, one for IPv4 and one for IPv6.

Hope that helps.

Well, could you help me by explaining how to do it? Well, I kept using ivp6 of the primary and secondary server

I know this is an older topic and I am necro-ing it... But, I just wanted to stop by and say thank you for the amazing writeup.

The reason I think that your vrrp_track_process isn't working is that even when pihole is disabled is that pihole-FTL is still sleeping, and the process doesn't exit to trigger that tracker. I personally only need this for a while a container or server is rebooting, so the process will be exited and the interface will be down lol.

However, I'd suggest that if you really need the functionality for it to failover while pihole-ftl is not running you should look into vrrp_script. A simple script that runs pihole status and greps/awk/whatever else for string
" [✓] Pi-hole blocking is enabled"
and returns successful should achieve what you were going for in your vrrp_track_process block.