Skip to main content

Technitium — Recursive DNS with Quad9 DoT

Project: Technitium DNS Server Host: technitium / technitium.home.lab IP: 10.69.20.53 VLAN: 20 (SERVERS) VMID: 253 Web UI: https://dns.home.helix9.org (via Traefik) — fallback http://10.69.20.53:5380


Overview

Technitium is the recursive DNS layer for the home network. It replaces VyOS's direct upstreams with an encrypted path: queries leave the network only over DNS-over-TLS to Quad9 (malware blocking + DNSSEC). Technitium also validates DNSSEC, caches responses, and hosts the block page served when DNS blocking matches a client query.

Query flow:

client → VyOS forwarder (zone gateway :53) → Technitium (10.69.20.53:53) → Quad9 (9.9.9.9:853, DoT)
· static-host overrides · blocklists + DNSSEC
· local cache · own cache

VyOS still resolves the *.home.helix9.org / *.home.lab static mappings locally before forwarding, so Traefik split-horizon keeps working.


Infrastructure

LXC Container

SettingValue
Nodepve02
VMID253
IP10.69.20.53/24
Gateway10.69.20.1
CPU2 cores
RAM2048 MB
Swap512 MB
Disk8 GB (SSD-Storage)
Templatedebian-13-standard_13.1-2
Unprivilegedyes

Ansible

Provisioned and managed via Ansible:

  • Playbook: playbooks/technitium.yml
  • Role: roles/technitium/
  • Host vars: inventory/host_vars/technitium/
  • Runtime: Podman Quadlet (systemd-managed container)
  • Image: docker.io/technitium/dns-server:latest
  • Data dir: /srv/technitium/config (mounted as /etc/dns inside container)

Podman Quadlet

systemctl status technitium
systemctl restart technitium
podman logs technitium

Published Ports

PortProtocolPurpose
53UDP/TCPDNS queries (from VyOS forwarder)
80TCPBlock Page app (HTTP)
443TCPBlock Page app (HTTPS, self-signed) / DoH listener
853TCPDoT listener (available, not in active use)
5380TCPWeb admin UI (behind Traefik)

Upstream — Quad9 DoT

Configured in Settings → Proxy & Forwarders:

SettingValue
Forwarder ProtocolDNS-over-TLS
Forwardersdns.quad9.net:853 (9.9.9.9)
dns.quad9.net:853 (149.112.112.112)
Concurrent Forwardingenabled (2 concurrent)
DNSSEC Validationenabled (Settings → General)

Service = Recommended (malware blocking + DNSSEC validation).

IPv4 only — do not add the IPv6 forwarders (2620:fe::fe, 2620:fe::9). This LXC has no IPv6 route, so v6 forwarders never reach Quad9 and every query that lands on one burns a full timeout before failing over → intermittent SERVFAIL, seen on clients as getaddrinfo ENOTFOUND (e.g. uptime-kuma flapping internal monitors Down then Up ~30s later). Keep forwarders Quad9 IPv4 only.

Other Quad9 service IPs (swap if you change policy):

ServiceIPv4TLS SAN
Recommended (malware + DNSSEC)9.9.9.9, 149.112.112.112dns.quad9.net
Secured w/ ECS9.9.9.11, 149.112.112.11dns11.quad9.net
Unsecured (no filtering/DNSSEC)9.9.9.10, 149.112.112.10dns10.quad9.net

Forwarders are seeded by the Ansible role on first deploy via the Technitium API (/api/settings/set). Manual changes in the UI persist across redeploys — the seed task skips when current settings already match.


DNSSEC

  • Validation: enabled globally (Settings → General → Enable DNSSEC Validation).
  • Verification:
    dig @10.69.20.53 dnssec-failed.org
    Expect status: SERVFAIL + EDE: 9 (DNSKEY Missing).
  • Caveat: devices without a real-time clock must bypass DNSSEC for NTP hostnames. Not applicable here — all clients have working RTCs.

Traefik Integration

Web UI reachable at https://dns.home.helix9.org via the existing Traefik reverse proxy.

Dynamic config in dynamic.yaml:

http:
routers:
dns:
rule: "host(`dns.home.helix9.org`)"
entrypoints: [websecure]
service: dns
tls:
certresolver: letsencrypt
priority: 100
services:
dns:
loadBalancer:
servers:
- url: "http://10.69.20.53:5380"

Traefik terminates TLS (LE cert) and proxies plain HTTP to Technitium's web port. Technitium's own HTTPS (5381) is not enabled.

VyOS static-host-mapping added so dns.home.helix9.org resolves to Traefik (10.69.20.67) for internal clients.


DNS Blocking

Blocklists

Managed in Settings → Blocking:

SettingValue
Blocking TypeCustom Address
Custom Blocking Addresses10.69.20.53

Lists pulled on a schedule from user-configured URLs (add in the Blocking tab). Allowed-domain overrides via top-nav Allowed tab.

Block Page

The Block Page app (installed from Apps → Store) serves an HTML page when a blocked domain is queried. Config (Apps → Block Page → Config):

{
"name": "default",
"enableWebServer": true,
"webServerLocalAddresses": ["0.0.0.0", "::"],
"webServerUseSelfSignedTlsCertificate": true,
"serveBlockPageFromWebServerRoot": true,
"webServerRootPath": "wwwroot"
}

Custom HTML lives at /srv/technitium/config/apps/Block Page/wwwroot/index.html on the LXC host (volume-mounted). JavaScript reads location.hostname to display the blocked domain on the page.

HTTPS Limitation

The Block Page serves a self-signed cert. For blocked HTTPS sites:

  • Regular HTTPS domains → browser shows cert warning → click-through → block page loads.
  • HSTS-preloaded domains (google, stake.com, major banks, etc.) → browser refuses any cert exception → permanent error, no click-through. This is inherent to HSTS + custom-address blocking and cannot be fixed without browser-level blocking (uBlock Origin).

Query Logging

Optional — install Query Logs (Sqlite) app (Apps → Store). Writes to /etc/dns/apps/ (persists via volume). View in Technitium's Logs tab.


Firewall

All cross-zone access to Technitium is governed by the VyOS zone policy matrix (see home-router.md §9).

From zoneTo SERVERSReaches Technitium?
LOCAL (router)ALLOW-ALLyes — DNS forwarding chains through
MGMTALLOW-ALLyes — full access
TRUSTEDALLOW-ALLyes — UI + block page
GUESTGUEST-SERVERSblock page only (TCP 80/443 carve-out)
HOMELAB / IOT / DMZALLOW-ESTno direct initiation — DNS via router

Technitium's egress to Quad9 (:853) is covered by SERVERS → WAN = ALLOW-INTERNET.


Credentials

  • Web UI user: admin
  • Password: vault_technitium_admin_password in inventory/host_vars/technitium/vault.yml (Ansible Vault).

Operations

Verify full chain

# From any client in TRUSTED
dig @10.69.40.1 example.org +short # resolves normally
dig @10.69.40.1 paperless.home.lab +short # local override → 10.69.20.72

DNSSEC (run from any client — the definitive test):

dig @10.69.40.1 dnssec-failed.org # MUST be status: SERVFAIL → validation enforced
dig @10.69.40.1 +dnssec cloudflare.com # flags line shows ad → answer authenticated

A bogus domain returning SERVFAIL proves validation (a non-validating resolver would hand back the bad A record). The ad flag reaching the client depends on the router doing set service dns forwarding dnssec validate — without it the VyOS forwarder strips ad, though Technitium still validates (the SERVFAIL still propagates).

DoT — Technitium runs as a rootful podman container on a bridge netns, so tcpdump/ss on the LXC host see nothing (no CAP_NET_RAW, separate netns). Look inside the container's network namespace:

# on technitium LXC
PID=$(sudo podman inspect technitium --format '{{.State.Pid}}')
sudo nsenter -t $PID -n ss -tn | grep :853
# → ESTABLISHED 10.88.0.6:xxxxx → 9.9.9.9:853 / 149.112.112.112:853 = DoT active

Gotcha: don't test cache-misses with random subdomains of DNSSEC-signed zones (e.g. *.example.com) — Technitium answers them from aggressive NSEC cache without any upstream query, so you'll wrongly see no :853. Use distinct real apex domains, or read the Dashboard query log. The Web UI (Settings → Proxy & Forwarders) also shows the live protocol (DNS-over-TLS) and forwarders directly.

Flush cache

UI → Cache tab → Flush Cache button. Or API:

curl -sk "http://10.69.20.53:5380/api/user/login?user=admin&pass=$PASS" | jq -r .token | \
xargs -I{} curl -sk "http://10.69.20.53:5380/api/cache/flush?token={}"

Switch Quad9 service / forwarders

Edit technitium_forwarders in roles/technitium/defaults/main.yml (or override in host_vars/technitium/vars.yml), then:

ansible-playbook playbooks/technitium.yml --tags seed

Rebuild container

ansible-playbook playbooks/technitium.yml

Data in /srv/technitium/config survives container replacement.


Known Issues / Caveats

Short-name resolution

The quadlet uses docker.io/technitium/dns-server:latest (fully qualified). Podman's strict short-name mode on Debian 13 rejects unqualified image names — don't drop the registry prefix.

HSTS block page

HSTS-preloaded domains (e.g. stake.com, google.com) cannot show the block page — browser refuses the self-signed cert without a click-through option. Outcome is "can't connect" rather than a pretty page. Accept as the block signal or switch blocking type to NX Domain for a cleaner fail UX at the cost of losing the page on HTTP/non-preloaded sites.

Bootstrap chicken-and-egg

If Technitium itself is down, VyOS forwarder has no upstream and internal DNS fails. Router's system name-server (1.1.1.1, 9.9.9.9) only serves router-local resolution (apt/NTP), not client queries. Mitigation: for extended outages, temporarily revert VyOS upstream:

configure
set service dns forwarding name-server 1.1.1.1
commit

Swap back once Technitium is healthy.