Running pihole on a VPS securely

I've been running pihole on my NAS for a while. My router is configured to use the NAS as its primary DNS, meaning all the devices on my network get configured to use the pihole resolver. Running it on my NAS server is not ideal though, because when I do maintenance on the NAS, all the devices on the network start having issues because the DNS resolver is down. I've tried providing a secondary DNS resolver to my router for cases like this, but it ends up making pihole useless because ads are able to get through on the devices even when the primary resolver is up.

So I decided to try to host pihole on an inexpensive VPS. The only issue is that it's not recommended to host a publicly accessible DNS resolver. It can be used in DNS amplification attacks. The plan to avoid this issue is to use a firewall which only allows IPs, and IP ranges I have vetted to access pihole. My home IP address is for the most part static, it hasn't changed it years as far as I can tell. This might not be a good solution if your home IP address changes often.

pihole setup

My VPS is running Debian 11 with a couple of services like my website, my blog, Gitea, and others. It already has docker installed on it. I installed pihole using a systemd unit file which simply spawns docker run:

# /etc/systemd/system/pihole.service
[Unit]
After=docker.service
Description=Pihole

[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/docker run --rm --name=pihole \
          -p 53:53/tcp -p 53:53/udp \
          -p 50010:80 \
          -v "/root/pihole/etc-pihole:/etc/pihole" \
          -v "/root/pihole/etc-dnsmasq.d:/etc/dnsmasq.d" \
          --dns 1.1.1.1 --dns=8.8.8.8 \
          --hostname pihole.sbstp.ca \
          -e TZ="America/Toronto" \
          -e WEBPASSWORD="<secret>" \
          -e CORS_HOSTS="pihole.sbstp.ca" \
          -e PIHOLE_DNS_="1.1.1.1;8.8.8.8" \
          docker.io/pihole/pihole:latest

[Install]
WantedBy=docker.service

Followed by systemctl enable pihole.service and systemctl start pihole.service. Port 50010 is for the pihole WebUI, my nginx server has a proxy pass to it when the hostname pihole.sbstp.ca is used.

ufw setup

The next step is to secure access to the resolver. I decided to use ufw (apt-get install ufw) for this task, but any firewall would do. Some VPS providers have built-in firewalls that can be used. Still, I prefer to control everything at the OS level. ufw is a frontend for iptables and it simplifies things a lot. It's a nice project.

There is one issue though, ufw and docker both play with iptable rules, and create imcompatible rules. Luckily though, there's a project called ufw-docker which has instructions on how to setup ufw properly.

The TL;DR is that you append the text below to /etc/ufw/after.rules, which makes docker and ufw get along.

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:ufw-docker-logging-deny - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN

-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN

-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] "
-A ufw-docker-logging-deny -j DROP

COMMIT
# END UFW AND DOCKER

After this you should restart ufw, e.g. systemctl restart ufw.

Now you can setup ufw:

  1. Allow ssh ufw allow ssh (shortcut for 22/tcp).
  2. Allow other ports such as 80, 443, etc. ufw allow 443/tcp.
  3. Allow udp packets coming from your home IP to be forwarded to the docker container ufw route allow proto udp from <HOME_IP> to any port 53. Note that this is using the internal docker port, not the external one. See ufw-docker for more details.
  4. Enable ufw, ufw enable.
  5. Check status, ufw status.

You can also allow friends & family to access the resolver by running the ufw route allow proto udp from <HOME_IP> to any port 53 command again and changing the IP to theirs.

Conclusion

You can test that everything works well by using dig (apt-get install bind9-utils). dig @<VPS IP> google.com resolves google.com using your DNS resolver. Make sure to run the dig command from within the network you want to run behind pihole.

Now that all that setup is done, you can set the VPS' IP to be the only DNS resolver in your router, and that should spread to all your devices in a few hours.