docs: rewrite for people running nxdns; query-history refusals name their remedy and a refused start gives no wrong hints
This commit is contained in:
@@ -1,247 +1,23 @@
|
||||
# Enable DoH and DoT
|
||||
|
||||
nxdns can answer encrypted queries on two extra listeners: DNS over HTTPS (`doh_server`) and DNS over TLS (`dot_server`). Both are off by default and both need a certificate and a private key in PEM form.
|
||||
Besides plain DNS on port 53, nxdns can answer encrypted queries from your devices: DNS over HTTPS (DoH) on port 443 and DNS over TLS (DoT) on port 853. Both are off by default, and both need a certificate and its private key, as PEM files.
|
||||
|
||||
This page uses a scratch lab under `/tmp/nxdns-lab` so the commands run without root and without touching a real install. On a real install the files live under `/etc/nxdns` and the data directory is `/var/lib/nxdns`; the ports are 443 and 853 rather than the unprivileged ones below.
|
||||
## 1. Get a certificate
|
||||
|
||||
Every field mentioned here is documented in [configuration reference](../reference/configuration.md).
|
||||
Devices check the certificate against the name you give them, so use a certificate for a name you control, from an ACME client such as certbot, lego or Caddy. Android's Private DNS setting, for example, takes a hostname and needs a certificate a phone trusts.
|
||||
|
||||
## 1. Get a certificate and key
|
||||
|
||||
For a LAN service the practical options are a certificate from your ACME client (certbot, lego, caddy) for a name you control, or a self-signed pair. The lab below uses a self-signed pair, because it needs no domain:
|
||||
|
||||
```sh
|
||||
mkdir -p /tmp/nxdns-lab/etc
|
||||
cd /tmp/nxdns-lab
|
||||
openssl req -x509 -newkey rsa:2048 -nodes \
|
||||
-keyout etc/key.pem -out etc/cert.pem -days 365 \
|
||||
-subj "/CN=nxdns.lan" \
|
||||
-addext "subjectAltName=DNS:nxdns.lan,DNS:localhost,IP:127.0.0.1"
|
||||
chmod 0600 etc/key.pem
|
||||
```
|
||||
|
||||
A self-signed certificate means every client has to be told to trust it, or told to skip verification. That is why the client commands further down pass `--insecure` and `+tls` without a CA. A real deployment uses a real certificate and drops those flags.
|
||||
|
||||
## 2. Turn the listeners on
|
||||
|
||||
`cert_path` and `key_path` must be absolute, or relative to the process working directory. Point both endpoints at the same pair unless you have a reason not to:
|
||||
|
||||
```zon
|
||||
.{
|
||||
.groups = .{.{ .name = "default" }},
|
||||
.upstreams = .{.{ .url = "https://cloudflare-dns.com/dns-query" }},
|
||||
.dns = .{ .bind_ipv4 = "127.0.0.1", .bind_ipv6 = "::1", .port = 15400 },
|
||||
.web = .{ .bind = "127.0.0.1", .port = 8451, .password = "lab-password" },
|
||||
.doh_server = .{
|
||||
.enabled = true,
|
||||
.bind = "127.0.0.1",
|
||||
.port = 8443,
|
||||
.cert_path = "/tmp/nxdns-lab/etc/cert.pem",
|
||||
.key_path = "/tmp/nxdns-lab/etc/key.pem",
|
||||
},
|
||||
.dot_server = .{
|
||||
.enabled = true,
|
||||
.bind = "127.0.0.1",
|
||||
.port = 8853,
|
||||
.cert_path = "/tmp/nxdns-lab/etc/cert.pem",
|
||||
.key_path = "/tmp/nxdns-lab/etc/key.pem",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Write that to `/tmp/nxdns-lab/etc/config.zon`. The lab runs `nxdns run --config`, which makes that file the configuration: every start reconciles the database onto it, so editing the file and restarting is how these settings change here. A real install may instead run bare `nxdns run` and keep the configuration in the database — see [the configuration model](../explanation/configuration-model.md).
|
||||
|
||||
## 3. Check the files before starting
|
||||
|
||||
```sh
|
||||
nxdns check --data-dir /tmp/nxdns-lab/data --config /tmp/nxdns-lab/etc/config.zon
|
||||
```
|
||||
|
||||
```
|
||||
checking configuration file /tmp/nxdns-lab/etc/config.zon
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
OK: no problems found
|
||||
```
|
||||
|
||||
`check` loads both endpoints' certificate and key the same way the listeners do, so what passes here will start. An unreadable file is a failure and exits 2:
|
||||
|
||||
```
|
||||
FAIL doh_server.cert_path: '/tmp/nxdns-lab/etc/cert.pem': certificate file is not readable
|
||||
FAIL dot_server.cert_path: '/tmp/nxdns-lab/etc/cert.pem': certificate file is not readable
|
||||
```
|
||||
|
||||
So is a key that does not belong to the certificate, which is the mistake worth catching before a restart — the two files are individually valid and only their pairing is wrong. mbedTLS writes its own line to stderr as it rejects the pair:
|
||||
|
||||
```
|
||||
warning(tls_server): mbedtls_pk_check_pair failed: RSA - Key failed to pass the validity check of the library (-16896)
|
||||
warning(tls_server): mbedtls_pk_check_pair failed: RSA - Key failed to pass the validity check of the library (-16896)
|
||||
checking configuration file /tmp/nxdns-lab/etc/mismatch.zon
|
||||
FAIL doh_server.key_path: '/tmp/nxdns-lab/etc/other.pem': private key does not belong to the certificate
|
||||
FAIL dot_server.key_path: '/tmp/nxdns-lab/etc/other.pem': private key does not belong to the certificate
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
```
|
||||
|
||||
A key readable by anyone but its owner is a warning instead. It does not change the exit code, because the service still starts, and the summary line counts it rather than claiming nothing was found:
|
||||
|
||||
```
|
||||
WARN doh_server.key_path: '/tmp/nxdns-lab/etc/key.pem' is mode 644; a TLS key must be readable by its owner only
|
||||
WARN dot_server.key_path: '/tmp/nxdns-lab/etc/key.pem' is mode 644; a TLS key must be readable by its owner only
|
||||
OK upstreams[0] https://cloudflare-dns.com
|
||||
OK: no failures found, 2 warnings
|
||||
```
|
||||
|
||||
## 4. Start and confirm the listeners
|
||||
|
||||
```sh
|
||||
nxdns run --data-dir /tmp/nxdns-lab/data --config /tmp/nxdns-lab/etc/config.zon
|
||||
```
|
||||
|
||||
Two lines in the log say the listeners bound:
|
||||
|
||||
```
|
||||
info(nxdns): doh listener on 127.0.0.1:8443
|
||||
info(nxdns): dot listener on 127.0.0.1:8853
|
||||
```
|
||||
|
||||
If a certificate cannot be loaded while its endpoint is enabled, nxdns refuses to start and exits 2 rather than serving DNS without the listener you asked for:
|
||||
|
||||
```
|
||||
doh_server: '/tmp/nxdns-lab/etc/cert.pem' + '/tmp/nxdns-lab/etc/key.pem': private key file is not readable
|
||||
nxdns run failed: BadCertificate
|
||||
run `nxdns check` to see the configuration in full
|
||||
```
|
||||
|
||||
## 5. Query DoT
|
||||
|
||||
Recent `dig` speaks DNS over TLS with `+tls` (this page was checked with BIND 9.20.26):
|
||||
|
||||
```sh
|
||||
dig @127.0.0.1 -p 8853 +tls example.com A +short
|
||||
```
|
||||
|
||||
```
|
||||
172.66.147.243
|
||||
104.20.23.154
|
||||
```
|
||||
|
||||
## 6. Query DoH
|
||||
|
||||
The only path the DoH listener serves is `/dns-query`; anything else is a 404. It accepts both the POST form (the query as an `application/dns-message` body) and the GET form (`?dns=` with base64url of the same bytes).
|
||||
|
||||
The body is a raw DNS query in wire format. Build one for `example.com A` — header with the recursion-desired bit, one question, then the QNAME as length-prefixed labels:
|
||||
|
||||
```sh
|
||||
printf '%s' '000001000001000000000000076578616d706c6503636f6d0000010001' \
|
||||
| xxd -r -p > /tmp/nxdns-lab/query.bin
|
||||
```
|
||||
|
||||
POST it:
|
||||
|
||||
```sh
|
||||
curl -sS --insecure --http1.1 \
|
||||
-H 'content-type: application/dns-message' \
|
||||
--data-binary @/tmp/nxdns-lab/query.bin \
|
||||
--output /tmp/nxdns-lab/answer.bin \
|
||||
-w 'http %{http_code}, %{size_download} bytes\n' \
|
||||
https://127.0.0.1:8443/dns-query
|
||||
xxd /tmp/nxdns-lab/answer.bin
|
||||
```
|
||||
|
||||
```
|
||||
http 200, 72 bytes
|
||||
00000000: 0000 8180 0001 0002 0000 0001 0765 7861 .............exa
|
||||
00000010: 6d70 6c65 0363 6f6d 0000 0100 01c0 0c00 mple.com........
|
||||
00000020: 0100 0100 0000 0400 04ac 4293 f3c0 0c00 ..........B.....
|
||||
00000030: 0100 0100 0000 0400 0468 1417 9a00 0029 .........h.....)
|
||||
00000040: 04d0 0000 0000 0000 ........
|
||||
```
|
||||
|
||||
The second flag byte `80` and the third answer-count field `0002` say: response, no error, two answer records.
|
||||
|
||||
The GET form takes the same bytes, base64url-encoded with the padding removed:
|
||||
|
||||
```sh
|
||||
Q=$(base64 -w0 /tmp/nxdns-lab/query.bin | tr '+/' '-_' | tr -d '=')
|
||||
curl -sS --insecure --http1.1 -o /tmp/nxdns-lab/get.bin \
|
||||
-w 'GET http %{http_code}, %{size_download} bytes\n' \
|
||||
"https://127.0.0.1:8443/dns-query?dns=$Q"
|
||||
```
|
||||
|
||||
```
|
||||
GET http 200, 61 bytes
|
||||
```
|
||||
|
||||
`--http1.1` matters: without it curl offers HTTP/2 over ALPN, and the DoH listener negotiates only what it advertises. `--insecure` is only needed for the self-signed lab certificate.
|
||||
|
||||
## 7. Renewals
|
||||
|
||||
A watcher polls both files every 30 seconds and compares their modification time and size against the pair currently loaded. When either differs it reloads and swaps the new pair in; connections already open finish on the old certificate. Nothing has to restart.
|
||||
|
||||
Replace the pair and wait one poll interval:
|
||||
To try it out first, a self-signed certificate is enough:
|
||||
|
||||
```sh
|
||||
openssl req -x509 -newkey rsa:2048 -nodes \
|
||||
-keyout /tmp/nxdns-lab/etc/key.pem -out /tmp/nxdns-lab/etc/cert.pem -days 365 \
|
||||
-keyout /etc/nxdns/key.pem -out /etc/nxdns/cert.pem -days 365 \
|
||||
-subj "/CN=nxdns.lan" \
|
||||
-addext "subjectAltName=DNS:nxdns.lan,DNS:localhost,IP:127.0.0.1"
|
||||
chmod 0600 /tmp/nxdns-lab/etc/key.pem
|
||||
-addext "subjectAltName=DNS:nxdns.lan,IP:192.168.1.10"
|
||||
```
|
||||
|
||||
Within 30 seconds the log says so, once per endpoint that watches the file:
|
||||
Replace `nxdns.lan` and `192.168.1.10` with the name and address your devices will use.
|
||||
|
||||
```
|
||||
info(cert_store): certificate reloaded from /tmp/nxdns-lab/etc/cert.pem
|
||||
info(cert_store): certificate reloaded from /tmp/nxdns-lab/etc/cert.pem
|
||||
```
|
||||
|
||||
## 8. Reload immediately
|
||||
|
||||
To skip the wait — from an ACME deploy hook, for example — call `POST /api/certs/reload`. It needs a session; see [set up admin authentication](set-up-admin-authentication.md) for the login call that fills `cookies.txt`.
|
||||
|
||||
```sh
|
||||
curl -sS -b /tmp/nxdns-lab/cookies.txt -X POST http://127.0.0.1:8451/api/certs/reload
|
||||
```
|
||||
|
||||
```json
|
||||
{"doh":{"enabled":true,"reloaded":true,"error":null},"dot":{"enabled":true,"reloaded":true,"error":null}}
|
||||
```
|
||||
|
||||
The route always answers 200: the per-endpoint outcome is the payload, not the status code. A disabled endpoint reports `"enabled":false`. A reload that fails names the reason and leaves the old certificate serving:
|
||||
|
||||
```sh
|
||||
chmod 000 /tmp/nxdns-lab/etc/key.pem
|
||||
curl -sS -b /tmp/nxdns-lab/cookies.txt -X POST http://127.0.0.1:8451/api/certs/reload
|
||||
```
|
||||
|
||||
```json
|
||||
{"doh":{"enabled":true,"reloaded":false,"error":"private key file is not readable"},"dot":{"enabled":true,"reloaded":false,"error":"private key file is not readable"}}
|
||||
```
|
||||
|
||||
The listeners keep working through that failure:
|
||||
|
||||
```sh
|
||||
dig @127.0.0.1 -p 8853 +tls example.com A +short
|
||||
```
|
||||
|
||||
```
|
||||
104.20.23.154
|
||||
172.66.147.243
|
||||
```
|
||||
|
||||
Undo it with `chmod 0600 /tmp/nxdns-lab/etc/key.pem` and reload again. The counters are on `/metrics`:
|
||||
|
||||
```
|
||||
nxdns_cert_reloads_total{endpoint="doh"} 3
|
||||
nxdns_cert_reloads_total{endpoint="dot"} 3
|
||||
nxdns_cert_reload_failures_total{endpoint="doh"} 2
|
||||
nxdns_cert_reload_failures_total{endpoint="dot"} 2
|
||||
```
|
||||
|
||||
## File ownership on a real install
|
||||
|
||||
The key must be readable by the user nxdns runs as, and by nobody else.
|
||||
|
||||
Under systemd the service runs as the `nxdns` user:
|
||||
The key must be readable by the user nxdns runs as, and by nobody else. Under systemd that is the `nxdns` user:
|
||||
|
||||
```sh
|
||||
chown nxdns:nxdns /etc/nxdns/cert.pem /etc/nxdns/key.pem
|
||||
@@ -249,17 +25,81 @@ chmod 0644 /etc/nxdns/cert.pem
|
||||
chmod 0600 /etc/nxdns/key.pem
|
||||
```
|
||||
|
||||
Under Docker the container runs as uid 65532, fixed in the image, and `/etc/nxdns` is a read-only bind mount — the container cannot fix permissions itself, so the host-side files must already be readable by that uid. It has no name on the host, so chown it numerically:
|
||||
Under Docker, put both files in `etc-nxdns/` next to `compose.yaml` and give them to uid 65532 the same way, since the container cannot change the read-only mount itself.
|
||||
|
||||
```sh
|
||||
cd deploy/docker
|
||||
chown 65532:65532 etc-nxdns/cert.pem etc-nxdns/key.pem
|
||||
chmod 0644 etc-nxdns/cert.pem
|
||||
chmod 0600 etc-nxdns/key.pem
|
||||
## 2. Turn the listeners on
|
||||
|
||||
In file mode, add both sections to the configuration file:
|
||||
|
||||
```zon
|
||||
.doh_server = .{
|
||||
.enabled = true,
|
||||
.cert_path = "/etc/nxdns/cert.pem",
|
||||
.key_path = "/etc/nxdns/key.pem",
|
||||
},
|
||||
.dot_server = .{
|
||||
.enabled = true,
|
||||
.cert_path = "/etc/nxdns/cert.pem",
|
||||
.key_path = "/etc/nxdns/key.pem",
|
||||
},
|
||||
```
|
||||
|
||||
**Not verified on this host:** the two `chown` blocks above. Both need root, and the systemd one needs an `nxdns` user this development machine does not have. Everything else on this page was executed as written.
|
||||
In database mode, open **System** in the web interface, tick **enabled** under **DoH Server** and **DoT Server**, check the certificate and key paths, and save.
|
||||
|
||||
## Ports 443 and 853
|
||||
Both listeners bind every address on their standard ports by default; `bind` and `port` change that. The packaged systemd unit already allows binding ports below 1024. Under Docker, uncomment the `443` and `853` lines under `ports:` in `compose.yaml`.
|
||||
|
||||
The defaults are the standard ports, which are privileged. Under the packaged systemd unit that is already handled: it grants `CAP_NET_BIND_SERVICE` for port 53 and the same capability covers 443 and 853. See [install with systemd](install-with-systemd.md).
|
||||
## 3. Check and restart
|
||||
|
||||
In file mode, `check` loads the certificate and key the same way the listeners will:
|
||||
|
||||
```sh
|
||||
nxdns check --config /etc/nxdns/config.zon
|
||||
```
|
||||
|
||||
It fails when a file is unreadable or when the key does not belong to the certificate, and warns when the key is readable by others:
|
||||
|
||||
```
|
||||
FAIL doh_server.key_path: '/etc/nxdns/other.pem': private key does not belong to the certificate
|
||||
WARN dot_server.key_path: '/etc/nxdns/key.pem' is mode 644; a TLS key must be readable by its owner only
|
||||
```
|
||||
|
||||
Then restart nxdns (`systemctl restart nxdns`, or `docker compose restart`). The log names both listeners:
|
||||
|
||||
```
|
||||
info(nxdns): doh listener on 0.0.0.0:443
|
||||
info(nxdns): dot listener on 0.0.0.0:853
|
||||
```
|
||||
|
||||
If a certificate cannot be loaded, nxdns refuses to start and exits 2 rather than run without the listener you asked for:
|
||||
|
||||
```
|
||||
doh_server: '/etc/nxdns/cert.pem' + '/etc/nxdns/key.pem': private key file is not readable
|
||||
nxdns run failed: BadCertificate
|
||||
```
|
||||
|
||||
## 4. Test them
|
||||
|
||||
DoT, with a `dig` from BIND 9.18 or newer:
|
||||
|
||||
```sh
|
||||
dig @192.168.1.10 +tls example.com A +short
|
||||
```
|
||||
|
||||
DoH, using curl's own DoH resolver to look up `example.com`:
|
||||
|
||||
```sh
|
||||
curl -sS --doh-url https://nxdns.lan/dns-query -o /dev/null \
|
||||
-w 'resolved to %{remote_ip}\n' https://example.com
|
||||
```
|
||||
|
||||
Add `--doh-insecure` to the curl command while you test with a self-signed certificate. DoH is served at `/dns-query` only, over HTTP/1.1, as both GET and POST.
|
||||
|
||||
## Renewing the certificate
|
||||
|
||||
nxdns checks both files every 30 seconds and switches to a renewed pair on its own; connections already open finish on the old one. There is nothing to restart. Each reload is logged:
|
||||
|
||||
```
|
||||
info(cert_store): certificate reloaded from /etc/nxdns/cert.pem
|
||||
```
|
||||
|
||||
To load it at once instead, press **Reload certificates** on the **System** page. If a reload fails, nxdns keeps serving the certificate it has and says why.
|
||||
|
||||
Reference in New Issue
Block a user