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>
231 lines
12 KiB
Markdown
231 lines
12 KiB
Markdown
# 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](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](#forwarding-over-tls) below.
|
|
|
|
## Setting it up
|
|
|
|
```sh
|
|
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:
|
|
|
|
1. **finds or obtains the certificate**: one Jarl manages whose names cover
|
|
the host name (`--certificate NAME` to 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 the `dns` use, so it is written
|
|
where named can read it, and named rereads it on every renewal.
|
|
2. **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`).
|
|
3. **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-on` statement at all. The first one, for DoT, would end that, so
|
|
when you have none the setup adds `listen-on { any; };` (and the same
|
|
for IPv6) to keep plain DNS where it was.
|
|
4. **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:
|
|
|
|
```sh
|
|
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}`:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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-hostname` and `ca-file`** make 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 only`** stops 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 `tls` statement each, and their addresses in one
|
|
`forwarders` list. Setting `tls cloudflare` doesn't touch `tls quad9`:
|
|
`jarl option set` replaces only the statement of that name.
|
|
|
|
```sh
|
|
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 internal
|
|
```
|
|
|
|
named 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 is
|
|
`remote-hostname`, and the addresses it resolves to go in `forwarders`
|
|
(named needs addresses there, not names).
|
|
|
|
**Checking it works**: Quad9 says how a query reached it.
|
|
|
|
```sh
|
|
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 `tls` statement 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 `dns` use: 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` |
|