Skip to content

Instantly share code, notes, and snippets.

Last active January 29, 2024 23:46
Show Gist options
  • Save bruvv/74b7c4d0b7e8111d5f1b1a9d02d147c7 to your computer and use it in GitHub Desktop.
Save bruvv/74b7c4d0b7e8111d5f1b1a9d02d147c7 to your computer and use it in GitHub Desktop.
Setup Pihole + Unbound + DNS over TLS on ubuntu 20.02 LTS


First enable swap just incase

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo cp /etc/fstab /etc/fstab.bak
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
sudo sysctl vm.swappiness=10
sudo sysctl vm.vfs_cache_pressure=50

Also add it to:

nano /etc/sysctl.conf

vm.swappiness = 10
vm.vfs_cache_pressure = 50

Update / Upgrade

apt-get update
apt-get -y install software-properties-common build-essential dialog rsyslog apt-utils

#sudo LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php
#apt-get update

apt-get -y full-upgrade

dpkg-reconfigure tzdata

apt-get install -y curl net-tools make wget php-fpm php-sqlite3 php-zip git man-db nano iptables-persistent nginx dnsutils python3-certbot-nginx libevent-dev libssl-dev libexpat1-dev cron python-pyinotify


Get it from here:

Lets setup UNBOUND:

mkdir ~/build-unbound
cd ~/build-unbound
tar -xvf unbound*
cd unbound-1.*/

useradd -r unbound

./configure \
        --with-pthreads \
        --with-username=unbound \
        --with-ssl \
        --with-libevent \
        --enable-tfo-server \
        --enable-tfo-client \
        --enable-event-api \

make && make install

ln -s /usr/local/etc/unbound/ /etc/unbound

mkdir /usr/local/etc/unbound/trust
#mkdir -p /etc/unbound/unbound.conf.d
chown unbound /usr/local/etc/unbound/trust

sudo -u unbound unbound-control-setup -d /usr/local/etc/unbound/trust/
sudo -u unbound unbound-anchor -a /usr/local/etc/unbound/trust/root.key

wget -O /etc/unbound/trust/root.hints
cat << EOM > /lib/systemd/system/unbound.service
Description=Unbound DNS server

ExecStartPre=/usr/local/sbin/unbound-anchor -a /usr/local/etc/unbound/trust/root.key
ExecStart=/usr/local/sbin/unbound -c /usr/local/etc/unbound/unbound.conf -d
ExecReload=/usr/local/sbin/unbound-control reload


systemctl daemon-reload

cat << EOM > /etc/cron.monthly/
wget -q -O /tmp/root.hints
if grep -q ROOT-SERVERS /tmp/root.hints ;then
  mv -f /tmp/root.hints /usr/local/etc/unbound/trust/root.hints ; chmod a+r /usr/local/etc/unbound/trust/root.hints ; chown unbound:unbound /etc/unbound/trust/*

chmod a+x /etc/cron.monthly/
rm /usr/local/etc/unbound/unbound.conf
wget -O /etc/unbound/unbound.conf
systemctl daemon-reload
systemctl unmask unbound
systemctl enable unbound
systemctl restart unbound

test unbound

netstat -lnp | grep unbound

dig @ -p 5353
dig @ -p 5353
dig @ -p 5353

service unbound status


We're going to set rate limits just in case the public gain access

cat << EOM > /etc/nginx/conf.d/00-rate-limits.conf
limit_req_zone \$binary_remote_addr zone=doh_limit:10m rate=300r/s;

Create the config - remember to replace server_name with whatever name you are using

cat << EOM > /etc/nginx/sites-available/dns-over-https
upstream dns-backend {
    keepalive 30;
server {
        listen 80;
        root /tmp/NOEXIST;
        location /dns-query {
                limit_req zone=doh_limit burst=50 nodelay;
                proxy_set_header X-Real-IP \$remote_addr;
                proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
                proxy_set_header Host \$http_host;
                proxy_set_header X-NginX-Proxy true;
                proxy_http_version 1.1;
                proxy_set_header Upgrade \$http_upgrade;
                proxy_set_header Connection "";
                proxy_redirect off;
                proxy_set_header X-Forwarded-Proto \$scheme;
                proxy_read_timeout 86400;
                proxy_pass http://dns-backend/dns-query;

sudo ln -s /etc/nginx/sites-available/dns-over-https /etc/nginx/sites-enabled/dns-over-https
sudo nginx -t
sudo systemctl reload nginx

Lets check if nginx is running correctly: netstat -lnp | grep 80

tcp        0      0    *               LISTEN      4890/nginx: master     

Lets setup stapeling

cat << EOM > /etc/nginx/conf.d/00-cert-stapling.conf
ssl_stapling on;
ssl_stapling_verify on;

certbot --nginx -d

cat << EOM > /etc/letsencrypt/options-ssl-nginx.conf
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
ssl_session_cache shared:le_nginx_SSL:1m;
ssl_session_timeout 1440m;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
# Enable modern TLS cipher suites
# The order of cipher suites matters
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Verify the next file: nano /etc/nginx/sites-available/dns-over-https

change the next line add http2 listen 443 ssl http2; # managed by Certbot CHANGE THIS

systemctl reload nginx

Setup Pi-Hole

(When prompted, do not install Pi-hole default firewall rules, make a note of the admin password when it's provided)


curl -sSL | bash

Change the password: sudo pihole -a -p CHANGEME

Fix the nginx.conf file:

mv /etc/nginx/nginx.conf /etc/nginx/nginx.bak

cat << EOM >  /etc/nginx/nginx.conf
#user root;
#user  nginx;
user www-data;
worker_processes auto;

#load_module modules/;

error_log  /var/log/nginx/error.log;
pid        /var/run/;

events {
    worker_connections  1024;

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    #sendfile    on;
    #tcp_nopush  on;

    keepalive_timeout  65;

    gzip  on;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/security.conf;
# DNS Stream Services
stream {

  # DNS logging
  log_format  dns   '$remote_addr [$time_local] $protocol "$dns_qname"';
  access_log /var/log/nginx/dns-access.log dns;

  # Include the NJS module
  #js_include /etc/nginx/njs.d/nginx_stream.js;

  # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing
  #js_set $dns_qname dns_get_qname;

  # DNS upstream pool.
  upstream dns {
    zone dns 64k;
  upstream dot {
    zone dns 64k;
  include /etc/nginx/streams/*.conf;
cat << EOM >  /etc/nginx/security.conf
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
cat << EOM >  /etc/nginx/conf.d/pihole.conf
server {
        root /var/www/html;
	listen 80;
        autoindex off;
        index pihole/index.php index.php index.html index.htm;
        access_log /var/log/nginx/pihole.access.log;

        location / {
                expires max;
                try_files $uri $uri/ =404;

	location ~ \.php$ {
		include fastcgi_params;
		fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
		fastcgi_pass unix:/run/php/php7.4-fpm.sock;
		fastcgi_param FQDN true;
		auth_basic "Restricted"; # For Basic Auth
		auth_basic_user_file /etc/nginx/.htpasswd; # For Basic Auth

        location /*.js {
                index pihole/index.js;
                auth_basic "Restricted"; #For Basic Auth
                auth_basic_user_file /etc/nginx/.htpasswd;  #For Basic Auth

        location /admin {
                root /var/www/html;
                index index.php index.html index.htm;
                auth_basic "Restricted"; #For Basic Auth
                auth_basic_user_file /etc/nginx/.htpasswd;  #For Basic Auth

        location ~ /\.ht {
                deny all;

        error_page 404 /pihole/index.php;
sudo nginx -t
sudo systemctl reload nginx

Verify that PiHole has the correct ip:

ifconfig|grep broad|awk -F' ' '{print $2}'

Check that that exists in: nano /etc/pihole/setupVars.conf

Lets configure FTL:

nano /etc/pihole/pihole-FTL.conf

Setup security: put it in: /etc/nginx/.htpasswd

certbot --nginx -d

nano /etc/nginx/conf.d/pihole.conf change the next line add http2 listen 443 ssl http2; # managed by Certbot CHANGE THIS

systemctl restart nginx

mkdir /etc/nginx/streams/

Now get from the DoH NGINX’s site configuration file the path to your HTTPS key and certificate.

cat << EOM > /etc/nginx/streams/dns-over-tls
stream {
	upstream dns-servers {
	  zone dns 64k;
	  server XX.XX.XX.XX:53; #PIHOLE IP can be found with: netstat -lnp | grep 53 DO NOT USE or LOCALHOST
	server {
	  listen 853 ssl; # managed by Certbot
	  proxy_bind $remote_addr transparent;
	  proxy_pass dns-servers;
	  ssl_preread on;

	  ssl_certificate /etc/letsencrypt/live/; # managed by Certbot
	  ssl_certificate_key /etc/letsencrypt/live/; # managed by Certbot
	  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
	  ssl_protocols        TLSv1.2 TLSv1.3;

	  ssl_prefer_server_ciphers       on;
	  ssl_handshake_timeout           10s;
	  ssl_session_cache               shared:DoT:10m;
	  ssl_session_timeout             1d;
	  ssl_session_tickets             off;

nano /etc/nginx/nginx.conf

include /etc/nginx/streams/*.conf;

sed -i ‘s/www-data/root/’ /etc/nginx/nginx.conf

usermod -a -G pihole www-data

Make sure you disable dns cache from pihole: nano /etc/dnsmasq.d/01-pihole.conf

service pihole-FTL restart
sudo systemctl restart nginx

setup FAIL2BAN

apt-get install fail2ban
cat << EOM > /etc/fail2ban/filter.d/pihole-dns.conf
 Fail2Ban configuration file
# Read common prefixes. If any customizations available -- read them from
# common.local
before = common.conf
_daemon = dnsmasq
# log example from /var/log/pihole.log
#Feb 26 04:41:28 dnsmasq[1887]: query[A] from
#(?:DAY )?MON Day 24hour:Minute:Second(?:\.Microseconds)?(?: Year)?
# This will filter all 'query' requests.
failregex = query\[.*\].* from <HOST>$
	    query\[ANY\].* from <HOST>$
	    query\[.*\] version\.bind from <HOST>$
	    query\[.*\] isc\.org from <HOST>$
	    query\[.*\] .*shadowserver.* from <HOST>$
	    query\[.*\] .* from <HOST>$
	    reply <HOST> is no-reverse-dns-configured\.com$
	    reply <HOST> is .*shodan\.io$
	    reply <HOST> is .*arbor-observatory.*$
	    reply <HOST> is .*stretchoid\.com$
	    reply <HOST> is .*openportstats\.com$
	    reply <HOST> is .*hostwindsdns\.com$
	    reply <HOST> is .*poneytelecom\.eu$

ignoreregex =
cat << EOM > /etc/fail2ban/jail.d/nginx.conf
enabled = true
filter = nginx-auth
action = iptables-multiport[name=NoAuthFailures, port="http,https"]
logpath = /var/log/nginx/*error*.log

enabled = false
filter = nginx-login
action = iptables-multiport[name=NoLoginFailures, port="http,https"]
logpath = /var/log/nginx/*access*.log
enabled  = true
filter = apache-badbots
action = iptables-multiport[name=BadBots, port="http,https"]
logpath = /var/log/nginx/*access*.log
maxretry = 1
enabled = true
action = iptables-multiport[name=NoProxy, port="http,https"]
filter = nginx-proxy
logpath = /var/log/nginx/*access*.log
maxretry = 0

enabled  = true
port     = http
filter   = nginx-dos
logpath  = /var/log/nginx/*access*.log
findtime = 120
maxretry = 200
cat << EOM > /etc/fail2ban/jail.d/pihole-dns.conf
enabled = true
port     = 53
action   = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp]
           %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp]
logpath = /var/log/pihole.log
findtime = 60
maxretry = 5
# bantime = 3600
cat << EOM > /etc/fail2ban/jail.d/ssh.conf
enabled = true
#port = 22
filter = sshd
#logpath = /var/log/auth.log
#maxretry = 3
systemctl start fail2ban
systemctl enable fail2ban

to check if fail2ban works fail2ban-client status pihole-dns

Test your DNS:

Secure server

sudo nano /etc/sysctl.conf Change / add this:

# IP Spoofing protection
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Ignore ICMP broadcast requests
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Disable source packet routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0 
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0

# Ignore send redirects
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0

# Block SYN attacks
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5

# Log Martians
net.ipv4.conf.all.log_martians = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1

# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0 
net.ipv6.conf.default.accept_redirects = 0

# Ignore Directed pings
net.ipv4.icmp_echo_ignore_all = 1

Secure SSH

cat << EOM > /etc/ssh/sshd_config.d/secure.conf
Protocol 2
PermitRootLogin no
DebianBanner no

Persistance bans for fail2ban:

touch /etc/fail2ban/ip.blocklist

nano /etc/fail2ban/jail.conf


bantime = X

Change it to:

bantime = -1

Edit action file:

nano /etc/fail2ban/action.d/iptables-multiport.conf

Add this to actionstart:

cat /etc/fail2ban/ip.blocklist | while read IP; do iptables -I f2b-<name> 1 -s $IP -j DROP; done

Add this to actionban:

echo <ip> >> /etc/fail2ban/ip.blocklist

Fix resolv

sudo systemctl stop systemd-resolved
sudo nano /etc/systemd/resolved.conf

Add this:


And restart:

sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf

sudo systemctl restart systemd-resolved

Clean stuff:

apt remove software-properties-common build-essential
apt autoremove
apt autoclean

Read extra nginx examples:

Copy link


Thank you for the write up, but I'm running into some issues.

After completing the entire guide, when I navigate to, I'm greeted by the Pi-hole landing page. Is that expected? I ask because, I thought the purpose of is for accessing the Pi-hole UI? Additionally, when I go to and click the "Did you mean to go to the admin panel", I get an error stating:

Failed Host Check: vs X.X.X.X, ,, pi.hole, localhost

Needless to say, when putting into my Android at this point, it gives the common "Couldn't connect" to the private DNS.

My second issue is that the certificiate for my is not valid. When investaging the certificate in the browser, it shows that the certificate is invalid because it's issued for and not I'm curious if that's due to needing to put the certificate information in /etc/nginx/streams/dns-over-tls. The guide mentions putting the certification information there.

Any ideas would be greatly appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment