DNS over TLS and HTTPS set up in one step

jarl encrypted-dns setup (and the Encrypted DNS page) finds or obtains a
certificate for the host name, checks that nothing else listens on the
DoT and DoH ports at the chosen addresses (offering a free DoH port), and
stages named's tls statement and listeners as one change, keeping port 53
where it was when there was no listen-on, plus the host name's records in
the private views (local to the view in a linked zone). Status shows what
to give Android, Linux, Firefox, Chrome and Windows clients. A tls
statement with missing files is refused (tls_file_missing), and a
certificate named uses can't be deleted. docs/encrypted-dns.md, e2e with
real DoT and DoH queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-12 17:09:14 +01:00
1 parent ebd3a3dfbe
commit 66dcf53684
21 files changed
+1790 -8

No files matched your search

+5
View File
@@ -556,6 +556,11 @@ Needs `stats:read`. When statistics are disabled, every endpoint but
curl -sf -H "$AUTH" "$API/stats/top?kind=nxdomain&since=7d&limit=10" | jq -r '.[] | "\(.count)\t\(.key)"'
```
### Encrypted DNS
See [encrypted-dns.md](encrypted-dns.md#api): `GET`, `POST` and `DELETE
/encrypted-dns`, `POST /encrypted-dns/check`.
### Certificates
Administrators only. See [certificates.md](certificates.md#api).
+6 -5
View File
@@ -183,16 +183,17 @@ configuration names them, Jarl runs `rndc reconfig`, and named serves the
new certificate at once (checked with BIND 9.20: DoT keeps working, with
the new certificate).
Until Jarl sets up DNS over TLS and HTTPS for you, add it by hand:
`jarl encrypted-dns setup` sets up DNS over TLS and HTTPS with it
([encrypted-dns.md](encrypted-dns.md)). By hand, a `tls` statement names
the files:
```sh
jarl option set tls 'dot { cert-file "/etc/bind/jarl/tls/ns1/fullchain.pem"; key-file "/etc/bind/jarl/tls/ns1/privkey.pem"; }' --toplevel
jarl option set listen-on 'port 853 tls dot { 10.0.10.5; }' --add # DoT
jarl option set listen-on 'port 443 tls dot http default { 10.0.10.5; }' --add # DoH at /dns-query
jarl option set listen-on 'port 853 tls dot { 10.0.10.5; }' --add
```
Listen on internal addresses only, unless you mean to run a public
resolver ([exposure.md](exposure.md)).
A certificate named uses can't be deleted or lose its `dns` use until
named no longer names its files.
## A certificate you renew yourself
+11
View File
@@ -409,6 +409,17 @@ jarl view update external --exposure public|private|auto
See [exposure.md](exposure.md).
## Encrypted DNS
```sh
jarl encrypted-dns # what is set up, what to give clients
jarl encrypted-dns setup --hostname dns.adbraun.lol --address 10.0.10.5 [--address IPv6] [--doh-port 8443] [--no-doh]
jarl encrypted-dns check --address 10.0.10.5 --doh-port 443
jarl encrypted-dns disable
```
See [encrypted-dns.md](encrypted-dns.md).
## Certificates
```sh
+131
View File
@@ -0,0 +1,131 @@
# 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`.
## Setting it up
```sh
jarl encrypted-dns setup --hostname dns.adbraun.lol --address 10.0.10.5 --address 2a02:8013:7f3d:10::5
```
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 name's A and AAAA records** with the chosen addresses,
in the private views that have a zone for it (`--view` to choose). In a
linked zone they are local to the view, so the name doesn't reach your
external view.
Running it again replaces the earlier setup (other addresses, ports, DoT
or DoH only with `--no-doh` / `--no-dot`). `jarl encrypted-dns disable`
takes the listeners and the `tls` statement out; the certificate and the
records stay.
| Option | Default | |
|--|--|--|
| `--hostname` | | The name clients connect to; the certificate is for it |
| `--address` | | This server's addresses to listen on (repeat). 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 the host name's records go |
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.
## 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`, `hostname`, `dot` and `doh` (address and port), `dot_host`, `doh_url`, `listeners`; for the form: `addresses` (this server's, with `private`), `views` (with `public`, and `has_zone` for `?hostname=`), `certificates` |
| POST | `/encrypted-dns/check` | `{"addresses", "dot", "doh", "dot_port", "doh_port"}`: `conflicts` (address, port, what) and `free_doh_port` |
| POST | `/encrypted-dns` | `{"hostname", "certificate", "addresses", "dot", "doh", "dot_port", "doh_port", "views"}`: staged or applied like any change; 409 while the certificate hasn't been obtained |
| DELETE | `/encrypted-dns` | Turns it off |
+6
View File
@@ -104,6 +104,12 @@ applied.
| `dnssec_old_keys` | info | Signing a zone whose key directory still has old keys |
| `serial_not_higher` | error | An explicit serial on a signed zone must be above the one named has loaded (automatic serials are raised by themselves) |
## TLS
| Code | Severity | Check |
|------|----------|-------|
| `tls_file_missing` | error | The files a top-level `tls` statement names (certificate, key, CA) exist: named doesn't load its configuration without them |
## Linked zones
See [views.md](views.md#linked-zones). Jarl keeps linked zones in step