jarl option set tls NAME (and key, remote-servers, primaries, http, dnssec-policy, key-store, server, acl, view, zone, parental-agents) matches the name after the keyword too: setting tls cloudflare no longer replaces tls quad9. jarl option unset tls NAME removes one by its name. docs/encrypted-dns.md has forwarding to Quad9 or Cloudflare over TLS: one changeset, the options explained, several providers, checking it works, and why not over HTTPS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
DNS over TLS and HTTPS
named (BIND 9.20) can answer over encrypted connections itself: DNS over
TLS (DoT, port 853) and DNS over HTTPS (DoH, at /dns-query). Your
phones, laptops and browsers then ask this server without anyone on the
network seeing or changing their queries, and they get exactly what plain
DNS gives them: the same views, the same records and the same blocking.
Jarl sets it up in one step: a certificate for the host name clients
connect to (obtained and renewed by Jarl, certificates.md),
named's tls statement and listeners on the addresses you choose, and the
host name's address records in your private views.
In the web UI: Encrypted DNS (administrators). In the CLI: jarl encrypted-dns. In the API: /api/v1/encrypted-dns.
That is encryption between your clients and this server. For encryption between this server and the resolvers it forwards to (Quad9, Cloudflare), see Forwarding over TLS below.
Setting it up
jarl encrypted-dns setup --hostname dns.adbraun.lol --address 10.0.10.5 --address 2a02:8013:7f3d:10::5
jarl encrypted-dns setup --hostname dns.adbraun.lol --address 10.0.10.5=internal --address 203.0.113.5=external
This:
-
finds or obtains the certificate: one Jarl manages whose names cover the host name (
--certificate NAMEto pick one), or a new one named after the host name's first label, which Jarl obtains from the CA (the command waits). The certificate gets thednsuse, so it is written where named can read it, and named rereads it on every renewal. -
checks the ports: nothing else may listen on the chosen addresses on port 853 or the DoH port. Jarl looks at the host's listening sockets (it binds nothing) and says what is in the way, e.g.
jarld (Jarl's web UI and API) listens on 10.0.10.5 port 443, and which port is free instead (--doh-port 8443). -
stages named's configuration, as one change you can review:
tls jarl-dns { cert-file ".../tls/dns/fullchain.pem"; key-file ".../tls/dns/privkey.pem"; protocols { TLSv1.2; TLSv1.3; }; }; options { listen-on port 853 tls jarl-dns { 10.0.10.5; }; listen-on port 443 tls jarl-dns http default { 10.0.10.5; }; listen-on-v6 port 853 tls jarl-dns { 2a02:8013:7f3d:10::5; }; ...named listens on port 53 on every address only while there is no
listen-onstatement at all. The first one, for DoT, would end that, so when you have none the setup addslisten-on { any; };(and the same for IPv6) to keep plain DNS where it was. -
adds the host names' A and AAAA records, each address in the views you give it:
--address 10.0.10.5=internal,lab, or every address in the views of--view(default: the private views that have a zone for the name). A view's clients should only be given an address they can reach, so a private address in a public view gets a warning. The records are local to their view (in a linked zone the other views keep theirs), and a view you no longer give an address loses it; other records of the names stay.
Several host names (--hostname dns.adbraun.lol --hostname doh.adbraun.lol, or comma separated): each gets the records, the
certificate must be for all of them (a new one is obtained with all of
them), and the health check says when the certificate named serves misses
one. Clients may use any of them; the client hints use the first.
Running it again replaces the earlier setup: other addresses, views, host
names, ports (clients set up with another port need theirs changed
too), DoT or DoH only with --no-doh / --no-dot. The web UI's form
starts from the current setup, with each address's views as its records
are now. jarl encrypted-dns disable takes the listeners and the tls
statement out; the certificate and the records stay.
| Option | Default | |
|---|---|---|
--hostname |
A name clients connect to (repeat for several); the certificate is for all of them | |
--address |
This server's addresses to listen on (repeat), with the views that get its records after = (10.0.10.5=internal). Choose your own networks' addresses: a listener on a public address answers anyone who reaches it, like port 53 there |
|
--certificate |
one for the host name, obtained if needed | A certificate Jarl manages |
--no-dot, --no-doh |
both on | |
--dot-port |
853 | Clients expect 853 |
--doh-port |
443 | Any free port; clients take it in the URL |
--view |
private views with the zone | Where every address's records go, when --address gives no views |
In the web UI the same form checks the ports as you choose, offers the free port, and obtains the certificate before setting up.
Which view clients get
A DoT or DoH client gets the view its address matches, as over plain DNS:
your phone at home gets the internal view, with its records and blocking.
Choosing a view by key (match-clients { key ...; }) doesn't work over
DoT and DoH, as clients don't sign their queries.
Clients
jarl encrypted-dns (and the page) shows what to give them:
For DoT clients: dns.adbraun.lol
For DoH clients: https://dns.adbraun.lol/dns-query
| Client | Where |
|---|---|
| Android 9+ | Settings → Network → Private DNS → Private DNS provider hostname: dns.adbraun.lol (DoT) |
| Linux (systemd-resolved) | /etc/systemd/resolved.conf: DNS=10.0.10.5#dns.adbraun.lol and DNSOverTLS=yes (or opportunistic, which falls back to plain DNS) |
| Firefox | Settings → Privacy & Security → DNS over HTTPS → Increased protection → Custom: the DoH URL |
| Chrome, Edge | Settings → Privacy and security → Security → Use secure DNS → custom: the DoH URL |
| Windows 11 | Settings → Network → your connection → DNS server assignment → Edit: the server's address, DNS over HTTPS on (manual template), the DoH URL |
Test from a machine on your network:
kdig +tls-ca +tls-host=dns.adbraun.lol @10.0.10.5 example.com
dig +https @10.0.10.5 example.com
curl -s -H 'accept: application/dns-json' 'https://dns.adbraun.lol/dns-query?name=example.com'
Android away from home. Private DNS in hostname mode applies on every
network. Away from home, your server can't be reached, and Android then
resolves nothing at all ("Private DNS server cannot be accessed"). Use a
VPN home (WireGuard) when you're out, or switch Private DNS to
Automatic when you leave. Firefox's Increased protection and
systemd-resolved's opportunistic fall back to normal DNS by themselves.
Clients check the certificate against the host name, so the name must resolve to this server for them: the setup adds it to your private views. Open the ports in the server's firewall for your own networks only.
Health
jarld checks the listeners itself, a minute after it starts and then
every 10 minutes, the way a client would: it connects to each, checks the
certificate named serves (for the host name, current, and the one Jarl
last wrote, so a renewal named hasn't reread shows up) and asks for the
host name. The Encrypted DNS page shows the result per listener (Check
now checks again), the dashboard a card and the problems, Prometheus
jarl_encrypted_dns_up{kind,address,port}:
jarl encrypted-dns health # check now; exit 1 when a listener doesn't work
What it can say:
| nothing listens on ADDR port P | named couldn't bind the port: look in named's log (ports below 1024 need named to start as root, as the Debian service does) |
| no answer within 5s | a firewall on the server |
| a certificate for X, not HOST | named serves another certificate |
| an older certificate | named hasn't reread the renewed files (rndc reconfig) |
| not from a CA this server trusts (warning) | a staging or test CA: clients reject it unless they trust it |
| REFUSED (warning) | it works, but this server's own address gets a view that doesn't answer for the host name |
Forwarding over TLS
When a view forwards its recursive queries (to Quad9, Cloudflare, your
ISP's resolver), named can send them over TLS, so your ISP doesn't see the
names you look up: only connections to the forwarder on port 853. It takes
two options, a tls statement for each provider (the name its certificate
must have) and the view's forwarders using it. Stage them in one
changeset, so one revert undoes them:
CS=$(jarl changeset create "internal: forward to Quad9 over TLS" -q)
jarl option set tls 'quad9 { remote-hostname "dns.quad9.net"; ca-file "/etc/ssl/certs/ca-certificates.crt"; }' --toplevel -C $CS
jarl option set forwarders '{ 9.9.9.9 port 853 tls quad9; 149.112.112.112 port 853 tls quad9; 2620:fe::fe port 853 tls quad9; 2620:fe::9 port 853 tls quad9; }' --view internal -C $CS
jarl option set forward only --view internal -C $CS
jarl changeset diff $CS # what named.conf gets
jarl changeset apply $CS
# undo: jarl changeset apply $(jarl changeset revert $CS -q)
-
remote-hostnameandca-filemake named check the forwarder's certificate against that name (Debian's CA bundle at that path). Without them the connection is encrypted but anyone in between could answer. -
forward onlystops named falling back to plain DNS (to the root servers) when the forwarders don't answer: lookups then fail instead (SERVFAIL), which you notice, rather than quietly going unencrypted. -
Several providers: a
tlsstatement each, and their addresses in oneforwarderslist. Settingtls cloudflaredoesn't touchtls quad9:jarl option setreplaces only the statement of that name.jarl option set tls 'cloudflare { remote-hostname "cloudflare-dns.com"; ca-file "/etc/ssl/certs/ca-certificates.crt"; }' --toplevel jarl option set forwarders '{ 9.9.9.9 port 853 tls quad9; 149.112.112.112 port 853 tls quad9; 1.1.1.1 port 853 tls cloudflare; 1.0.0.1 port 853 tls cloudflare; 2606:4700:4700::1111 port 853 tls cloudflare; 2606:4700:4700::1001 port 853 tls cloudflare; }' --view internalnamed sends each query to whichever forwarder has answered fastest, not in order, and the providers filter differently (Quad9 blocks known malicious domains, Cloudflare's 1.1.1.1 doesn't; 1.1.1.2 does, with
remote-hostname "security.cloudflare-dns.com"). -
DNS over HTTPS can't be used for forwarding: BIND serves DoH to clients but forwards only over plain DNS or TLS. For a DoH upstream, run a local proxy (dnscrypt-proxy, cloudflared) and forward to it on
127.0.0.1. -
Providers write their DoT address as
tls://dns.quad9.net: the name isremote-hostname, and the addresses it resolves to go inforwarders(named needs addresses there, not names).
Checking it works: Quad9 says how a query reached it.
dig +short TXT proto.on.quad9.net @YOUR-SERVER # "dot": over TLS
do53-udp means plain DNS. ss -tnp | grep :853 shows named's connections
only briefly: it closes them when idle. tcpdump -ni any port 853 shows
the traffic as it happens.
What Jarl checks
- A
tlsstatement whose files don't exist is an error (tls_file_missing): named won't load its configuration without them. - A certificate named uses can't be deleted, nor lose its
dnsuse: turn encrypted DNS off first. - Listeners you wrote yourself are left alone; the page notes them.
API
| Method | Path | Body |
|---|---|---|
| GET | /encrypted-dns |
enabled, certificate, hostnames (and hostname, the first), dot and doh (address and port), address_views (per address, the views whose records give a host name it), dot_host, doh_url, listeners; for the form: addresses (this server's, with private), views (with public, and has_zone), certificates. ?hostname=a,b asks about other names |
| POST | /encrypted-dns/check |
{"addresses", "dot", "doh", "dot_port", "doh_port"}: conflicts (address, port, what) and free_doh_port |
| POST | /encrypted-dns |
{"hostnames", "certificate", "addresses", "address_views": {"10.0.10.5": ["internal"]}, "dot", "doh", "dot_port", "doh_port"} ("hostname" and "views", every address in each, work too): staged or applied like any change; 409 while the certificate hasn't been obtained |
| DELETE | /encrypted-dns |
Turns it off |
| POST | /encrypted-dns/health |
Checks the listeners now: checked, probes (kind, address, port, ok, problem, warning, certificate names, expiry and serial, trusted, rcode, ms). GET /encrypted-dns has the last check as health |