Two self-contained Bash installers for standing up a public, encrypted DNS resolver on a Debian/Ubuntu server — one built on dnsdist, the other on BIND9. Both expose DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT) and obtain their own Let's Encrypt certificates. Pick whichever backend you prefer; you do not need both.
dnsdist-setup.sh |
bind9-setup.sh |
|
|---|---|---|
| Role | DoH/DoT front-end that forwards to public resolvers | Full recursive/forwarding resolver |
| Cert subject | Server's public IP (RFC 8738 IP cert) | A domain name you point at the server |
| Upstreams | Google / Cloudflare / Quad9 over DoH | Two forwarders you choose (default 8.8.8.8 / 1.1.1.1) |
| Ad-blocking | Optional dnsdist SuffixMatchNode blocklist |
Optional BIND RPZ zone (hagezi pro) |
| Cert tool | acme.sh (6-day short-lived certs) |
certbot (standalone) |
Both scripts must run as root, target Debian 11/12 and Ubuntu 20.04–24.04, and need inbound port 80 reachable during issuance plus 443/853 open afterward.
A thin encrypted front-end. dnsdist terminates DoH (443) and DoT (853) on the server's public IP and load-balances queries out to Google, Cloudflare and Quad9 over validated outgoing DoH.
What it does
- Detects the public IPv4 (
ifconfig.me) and installsdnsdistfrom the official PowerDNS repo, plusacme.sh. - Issues a Let's Encrypt IP-address certificate (short-lived, 6-day profile) via the standalone http-01 challenge, and installs a 12-hour renewal cron job.
- Writes
/etc/dnsdist/dnsdist.confwith a packet cache, QPS-per-IP rate limiting, andbind./server.chaos-query hardening. - (optional) Prompts to enable an ad/tracker blocklist, downloaded to
/etc/dnsdist/blocklist.txtand refreshed daily by cron. - Grants
CAP_NET_BIND_SERVICEvia a systemd drop-in and starts the service.
Run
sudo ./dnsdist-setup.shEndpoints
- DoH →
https://<public-ip>/dns-query - DoT →
tls://<public-ip>:853
No domain name required — the certificate is issued for the IP itself.
A complete BIND9 resolver serving plain DNS (53), DoT (853) and — on BIND 9.18+ — DoH (443) for a domain you control.
What it does
- Prompts for the domain, two upstream forwarders, optional RPZ ad-block, and an optional stats channel.
- Lets you choose where BIND9 comes from (see below).
- Obtains a Let's Encrypt certificate for the domain via
certbot --standaloneand installs a renewal deploy-hook that reloads BIND. - Writes a hardened
named.conf.*set (DNSSEC validation, rate limiting, cache tuning, minimal version/hostname disclosure) and the TLS/HTTP listeners. - (optional) Deploys an RPZ zone (hagezi
dns-blocklistspro) with a systemd timer that refreshes it on a schedule you pick. - Configures logging/logrotate, opens the firewall (UFW or iptables), starts
named, and runs adigsmoke test.
Choosing the BIND9 version
Early in the run the script asks where BIND should come from:
-
Distro mirror (apt) — fast; installs whatever your release ships. If that's older than 9.18 (so no DoH) it tries Debian backports automatically.
-
Build a specific version from ISC source — fetches the live list of releases from
downloads.isc.org, lets you pick one (or type an exact9.x.y), verifies the SHA512 checksum, and compiles it with DoH (libnghttp2) support. You're then asked whether to overwrite the distro binaries in/usr(clean, but a laterapt upgradecould replace them) or install under/usr/local(a systemd drop-in repointsnamed.serviceat the built binary, surviving apt upgrades).In both cases the distro
bind9package is installed first to provide thebinduser,/etc/bindskeleton,rndc.keyand basenamed.service.
Run
sudo ./bind9-setup.shEndpoints
- Plain DNS →
<domain>:53(TCP/UDP) - DoT →
<domain>:853 - DoH →
https://<domain>/dns-query(BIND 9.18+ only)
bind9_setup.py is a faithful, modern rewrite of bind9-setup.sh using
asyncio. It performs the exact same steps — package installs, optional source
build, certbot issuance, named.conf.* generation, RPZ timer, firewall, smoke
test — but runs subprocesses and downloads on the event loop, overlapping the
ISC version fetch with the interactive prompts and the tarball/checksum
downloads with each other.
sudo ./bind9_setup.py # same prompts and behaviour as the bash scriptrun_tests.py orchestrates three stages with a dynamic, in-place CLI:
- Ruff lint over the sources.
- Unit tests in
tests/(pure renderers, version parsing, async helpers). - Live Podman integration — spins up
debian:trixie-slimand drives the real install routines for both methods (apt mirror and ISC source build), assertingnamedends up installed and executable.
On a free-threaded (no-GIL) interpreter the stages run concurrently across CPU
cores via threading; otherwise they fall back to sequential execution. The
runner animates a spinner per running stage and prints a summary box with
totals, dumping failure logs and aborting non-zero if any stage fails.
uv venv --python python3.14t .venv
uv pip install --python .venv/bin/python ruff pytest
.venv/bin/python run_tests.py # all stages
.venv/bin/python run_tests.py --no-podman # skip the container stages- Run as root on Debian/Ubuntu.
- Point the relevant DNS/A record (BIND) or just have a public IP (dnsdist) at the server, and ensure port 80/tcp is free for ACME validation.
- Certificates renew automatically (cron for dnsdist, certbot deploy-hook for BIND); the service reloads only when a cert actually changes.
- Building BIND from source can take several minutes.
If you find this project useful, consider donating:
- TON network:
UQB0t1EOgP2SZOfWK4HMZc_hda0lczGCJzH6-cvuv2ZuFhD7 - TRC20 network:
TYCRxNPqYv342hr4yDzYjm48ruAvL8joj4 - BNB network:
0x0E83d0d058B1A10F079D46fa70760068E305D6eB