Skip to content

Instantly share code, notes, and snippets.

@bruvv
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

Swap

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

firewall

Get it from here: https://gist.github.com/bruvv/4b1283fa7902447b3d2ae69481ea8ffa

Lets setup UNBOUND:

mkdir ~/build-unbound
cd ~/build-unbound
wget http://www.unbound.net/downloads/unbound-latest.tar.gz
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 \
	--enable-subnet

make && make install

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

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 https://www.internic.net/domain/named.root
cat << EOM > /lib/systemd/system/unbound.service
[Unit]
Description=Unbound DNS server
Documentation=man:unbound(8)
After=network.target
Before=nss-lookup.target
Wants=nss-lookup.target

[Service]
Type=simple
Restart=on-failure
EnvironmentFile=-/etc/default/unbound
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

[Install]
WantedBy=multi-user.target
EOM

systemctl daemon-reload

cat << EOM > /etc/cron.monthly/update-unbound-hints.sh
#!/bin/bash
wget -q https://www.internic.net/domain/named.root -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/*
fi
EOM

chmod a+x /etc/cron.monthly/update-unbound-hints.sh
/etc/cron.monthly/update-unbound-hints.sh
rm /usr/local/etc/unbound/unbound.conf
wget -O /etc/unbound/unbound.conf https://gist.githubusercontent.com/bruvv/41577f18732b2bfb9ab18fe0581b588a/raw/7ebcd72f14b132ff90a35efeb06b18c3f04b7289/unbound.conf
systemctl daemon-reload
systemctl unmask unbound
systemctl enable unbound
systemctl restart unbound

test unbound

netstat -lnp | grep unbound

dig pi-hole.net @127.0.0.1 -p 5353
dig sigfail.verteiltesysteme.net @127.0.0.1 -p 5353
dig sigok.verteiltesysteme.net @127.0.0.1 -p 5353

service unbound status

Setup NGINX

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;
EOM

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 {
    server 127.0.0.1:53;
    keepalive 30;
}
server {
        listen 80;
        server_name dns.domain.com;
        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;
        }   
}
EOM

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 0.0.0.0:80              0.0.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;
resolver 127.0.0.1:5353;
EOM

certbot --nginx -d dns.domain.com

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
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
# The order of cipher suites matters
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
EOM

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)

DNS custom 127.0.0.1#5353 DO NOT INSTALL THE WEBSERVER we use nginx.

curl -sSL https://install.pi-hole.net | 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/ngx_stream_js_module.so;

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

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;
    server 127.0.0.1:53;
  }
  upstream dot {
    zone dns 64k;
    server 10.0.0.30:53;
  }
  include /etc/nginx/streams/*.conf;
}
EOM
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;
EOM
cat << EOM >  /etc/nginx/conf.d/pihole.conf
server {
        root /var/www/html;
	listen 80;
        server_name dnsadmin.domain.com;
        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;
}
EOM
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
PRIVACYLEVEL=0
DBINTERVAL=60
RESOLVE_IPV4=yes
BLOCKINGMODE=NULL
IGNORE_LOCALHOST=yes

Setup security: https://www.htaccesstools.com/htpasswd-generator/ put it in: /etc/nginx/.htpasswd

certbot --nginx -d dnsadmin.domain.com

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 127.0.0.1 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/dns.domain.com/fullchain.pem; # managed by Certbot
	  ssl_certificate_key /etc/letsencrypt/live/dns.domain.com/privkey.pem; # managed by Certbot
	  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
	  ssl_protocols        TLSv1.2 TLSv1.3;

	  #ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-$
	  ssl_prefer_server_ciphers       on;
	  ssl_handshake_timeout           10s;
	  ssl_session_cache               shared:DoT:10m;
	  ssl_session_timeout             1d;
	  ssl_session_tickets             off;
	}
}
EOM

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
[INCLUDES]
# Read common prefixes. If any customizations available -- read them from
# common.local
before = common.conf
[Definition]
_daemon = dnsmasq
# log example from /var/log/pihole.log
#Feb 26 04:41:28 dnsmasq[1887]: query[A] 21cl93vlx5n9p.aikoaiko.net from 67.21.36.3
#(?: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\[.*\] .*shodan.io 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 =
EOM
cat << EOM > /etc/fail2ban/jail.d/nginx.conf
[nginx-auth]
enabled = true
filter = nginx-auth
action = iptables-multiport[name=NoAuthFailures, port="http,https"]
logpath = /var/log/nginx/*error*.log

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

[nginx-dos]
enabled  = true
port     = http
filter   = nginx-dos
logpath  = /var/log/nginx/*access*.log
findtime = 120
maxretry = 200
EOM
cat << EOM > /etc/fail2ban/jail.d/pihole-dns.conf
[pihole-dns]
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
EOM
cat << EOM > /etc/fail2ban/jail.d/ssh.conf
[sshd]
enabled = true
#port = 22
filter = sshd
#logpath = /var/log/auth.log
#maxretry = 3
EOM
systemctl start fail2ban
systemctl enable fail2ban

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

Test your DNS: https://rootcanary.org/test.html

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
EOM

Persistance bans for fail2ban:

touch /etc/fail2ban/ip.blocklist

nano /etc/fail2ban/jail.conf

Find:

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:

[Resolve]
DNS=127.0.0.1
FallbackDNS=9.9.9.9
#Domains=
#LLMNR=no
#MulticastDNS=no
#DNSSEC=no
#Cache=yes
DNSStubListener=no

And restart:

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

sudo systemctl restart systemd-resolved

Clean stuff:

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

Read extra nginx examples: https://github.com/TuxInvader/nginx-dns

@crunchprank
Copy link

Hello,

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

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

Failed Host Check: dns.domain.com vs X.X.X.X, , dnsadmin.domain.com, pi.hole, localhost

Needless to say, when putting dns.domain.com 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 dnsadmin.domain.com is not valid. When investaging the certificate in the browser, it shows that the certificate is invalid because it's issued for dns.domain.com and not dnsadmin.domain.com. I'm curious if that's due to needing to put the dnsadmin.domain.com certificate information in /etc/nginx/streams/dns-over-tls. The guide mentions putting the dns.domain.com 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