I wanted to run the pi-hole web admin over HTTPS on the default SSL port (443), as well as serve DNS-over-HTTPS (DoH) traffic on the same port (443) to streamline usage of both. Since DoH support isn't currently in pihole, this meant running some 2nd service next to the pihole web server with some reverse proxy sitting in front of both of those on port 443, routing traffic appropriately.
Complicating this was that I wanted to have EDNS Client Subnet information survive the proxying process so that the pihole could still classify DoH clients. This meant the reverse proxy and the DoH server needed to add and/or preserve that information so that it was present for pihole to consume when EDNS0_ECS=true in the configuration.
The solution I got working was an nginx
reverse proxy in front of pihole's lighttpd
& dnsdist
.
1. Get a Valid SSL Cert
Get a valid SSL certificate for your pi-hole. HMN76V's original post (archive) linked to this tutorial (archive), which worked for me.
2. dnsdist
dnsdist will be the DNS-over-HTTPS endpoint that will terminate DoH queries, attach client_subnet info, and forward them to pihole. We'll use the trustForwardedForHeader
option to have dnsdist
take the IP from an X-Forwarded-For
header as the IP to use in the EDNS Client Subnet.
The dnsdist
in the pihole's package manager will work; install with apt-get install dnsdist
Edit or create /etc/systemd/system/dnsdist.service
:
[Unit]
Description=dnsdist DoH Server
Documentation=man:dnsdist(1)
Documentation=https://dnsdist.org
Wants=network-online.target
After=network-online.target
[Service]
ExecStartPre=/usr/bin/dnsdist --check-config
# Note: when editing the ExecStart command, keep --supervised and --disable-syslog
ExecStart=/usr/bin/dnsdist --supervised --disable-syslog -C /etc/dnsdist/dnsdist.conf
User=_dnsdist
Group=ssl-servers
SyslogIdentifier=dnsdist
Type=notify
Restart=on-failure
RestartSec=2
TimeoutStopSec=5
StartLimitInterval=0
# Tuning
TasksMax=8192
LimitNOFILE=16384
# Note: increasing the amount of lockable memory is required to use eBPF support
# LimitMEMLOCK=infinity
# Sandboxing
# Note: adding CAP_SYS_ADMIN (or CAP_BPF for Linux >= 5.8) is required to use eBPF support,
# and CAP_NET_RAW to be able to set the source interface to contact a backend
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
LockPersonality=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
# Setting PrivateUsers=true prevents us from opening our sockets
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=full
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=~ @clock @debug @module @mount @raw-io @reboot @swap @cpu-emulation @obsolete
[Install]
WantedBy=multi-user.target
Edit or create /etc/dnsdist/dnsdist.conf
:
-- dnsdist configuration file, an example can be found in /usr/share/doc/dnsdist/examples/
-- listen for DNS-over-HTTPs queries on 3054, authd by our pihole cert
addDOHLocal(
'192.168.1.254:3054',
'/usr/local/ssl/crt/pi.hole.crt',
'/usr/local/ssl/crt/pi.hole.key',
'/dns-query',
{
trustForwardedForHeader=true
})
-- prepare to forward DNS queries to pihole w/ client subnet info
pihole = newServer({
address="192.168.1.254:53",
name="pihole",
useClientSubnet=true
})
pihole:setUp()
-- send full client IP as subnet
setECSSourcePrefixV4(32)
-- disable security status polling via DNS
setSecurityPollSuffix("")
Change
192.168.1.254
to your pihole's static IP/usr/local/ssl/crt/pi.hole.crt
to your pihole's SSL certificate/usr/local/ssl/crt/pi.hole.key
to your pihole's SSL key that signed the cert
NOTE: do not use 127.0.0.1
for the newServer
; while this will work to resolve DNS queries, it will cause the pihole to respond with that localhost IP for pi.hole
lookups and you won't be able to browse the web admin at pi.hole
!
NOTE: pihole:setUp()
disables dnsdist
's health-check queries to pihole. In this setup there is no alternative to pihole, and it's not dnsdist
's job to solve pihole being down - queries should always go to pihole and if pihole's down they just won't resolve!
3. nginx
nginx
will be the SSL-terminating reverse-proxy in front of both the dnsdist
DoH endpoint and the pihole web admin. We'll take the real IP of clients connecting to nginx
and put them in the X-Forwarded-For
HTTP header before sending the request along to dnsdist
.
The nginx
that is in pihole's package manager will work; apt-get install nginx
.
Edit or create /etc/systemd/system/nginx.service
:
[Unit]
Description=nginx
After=syslog.target network.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /etc/nginx/conf.d/https-reverse-proxy.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /etc/nginx/conf.d/https-reverse-proxy.conf
ExecReload=/usr/bin/kill -s HUP $MAINPID
ExecStop=/usr/bin/kill -s QUIT $MAINPID
# Hardening
InaccessiblePaths=/etc/shadow /etc/ssh
ProtectSystem=full # /usr, /boot/, /efi read-only!
ProtectKernelTunables=yes
ProtectControlGroups=yes
SystemCallFilter=~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictRealtime=yes
[Install]
WantedBy=multi-user.target
Change
192.168.1.254
to your pihole's static IP
Create /etc/nginx/conf.d/https-reverse-proxy.conf
:
(this config sets X-Real-IP
too, for good measure)
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
events {}
http {
access_log /var/log/nginx/http-access.log;
error_log /var/log/nginx/http-error.log;
# dnsdist https
upstream doh-ecs {
zone doh-ecs 64k;
server 127.0.0.1:3054;
}
# pihole lighttpd
upstream pi.hole {
zone pihole-web 64k;
server 192.168.1.254:80;
}
# This virtual server accepts HTTP/2 over HTTPS
server {
listen *:443 ssl http2;
server_name pi.hole pihole.local 192.168.1.254;
ssl_certificate /usr/local/ssl/crt/pi.hole.crt;
ssl_certificate_key /usr/local/ssl/crt/pi.hole.key;
access_log /var/log/nginx/ssl-proxy-access.log;
error_log /var/log/nginx/ssl-proxy-error.log;
# send all traffic to pihole's web-ui by default
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://pi.hole;
access_log /var/log/nginx/https-to-pihole.log;
}
# send /dns-queries to dnsdist to add ecs info
location /dns-query {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass https://doh-ecs;
access_log /var/log/nginx/dns-query.log;
}
}
}
Change:
192.168.1.254
to your pihole's static IP/usr/local/ssl/crt/pi.hole.crt
to your pihole's SSL certificate/usr/local/ssl/crt/pi.hole.key
to your pihole's SSL key that signed the cert- the
server.name
line to list all the Subject Alternative Names (SAN) that you provided for your pihole in its certs. This must at the very least includepi.hole
- (the static IP of the pihole)
4. done!
Remember to systemctl enable nginx
and systemctl enable dnsdist
so they'll start when the pihole does.
With that, assuming a pihole IP of 192.168.1.254
, you should find:
192.168.1.254
works as pihole DNS, per normalhttps://pi.hole
redirects tohttps://pi.hole/admin
& serves the web interface over HTTPS with your certificate- The pihole web interface remains accessible over HTTP on port 80
- Disabling this left as an exercise to the reader (for now?)
https://192.168.1.254/dns-query
works for DNS-over-HTTPS queries, with your certificate- DoH queries are correctly mapped to individual clients in the pihole logs
Bonus
Go forth and use DoH!
Set Windows 11 to use DoH:
(this took me far too long to figure out; couldn't find any clear instructions anywhere, either)
Network & internet > Ethernet
--> "DNS Server Assignment" section
Set brave browser to use DoH
Search "dns" in settings, then configure a custom DNS provider: