Comprehensive documentation gardening across the merged repo: - tailscale.md: fully rewritten with current ground truth. The netinfra pair now runs production Technitium with all knel.net records replicated. Both LAN IPs resolve knel.net device names and recurse externally. The old "NXDOMAIN / zone is stale" findings are replaced with the resolved state and current recommendations. - AGENTS.md: rewritten with Gitea-compatible clickable relative links to all key scripts and docs. Autonomous commit/push policy prominently documented. SSH user corrected to localuser. - README.md: directory table and docs table now use clickable links. - All .md cross-references converted to Gitea-renderable relative links. - Stale path references (ProjectCode/, Project-Tests/, ProjectDocs/) updated to current names (provisioning/, tests/) across all docs. - Stale repo name "FetchApply" / "KNELServerBuild" updated to "PFVCluster" in actionable docs; historical AI-review docs tagged with an HTML comment notice. - REFACTORING-EXAMPLES.md: tagged as historical (pre-refactor patterns). - tests/README.md, dns-cluster-setup/README.md, docs/DEPLOYMENT.md, docs/SECURITY.md: path references fixed to current structure. 🤖 Generated with [Crush](https://github.com/charmassociates/crush) Assisted-by: GLM-5 via Crush <crush@charm.land>
7.4 KiB
Technitium DNS Cluster Setup
Replicates the production Technitium DNS Server from tailscale-router to the
pfv-netinfra-01/02 pair and configures them as a primary/secondary cluster
with automatic zone transfers.
Architecture
tailscale-router (PRODUCTION — READ ONLY)
└─ tsys-dns container (technitium/dns-server)
└─ 124 zones (knel.net + reverse DNS)
└─ Users + 2FA in auth.config
│
docker cp (export)
│
▼
┌─ pfv-netinfra-01 (192.168.3.252) ──── PRIMARY ──────────┐
│ tsys-dns container (Technitium on :5300) │
│ pihole container (Pi-hole on :53 → Technitium :5300) │
│ All zones are Primary │
│ Zone transfer allowed from 192.168.3.253 │
└──────────────────────────────────────────────────────────┘
│
AXFR / IXFR + NOTIFY (DNS zone transfer, port 5300)
│
▼
┌─ pfv-netinfra-02 (192.168.3.253) ─── SECONDARY ────────┐
│ tsys-dns container (Technitium on :5300) │
│ pihole container (Pi-hole on :53 → Technitium :5300) │
│ All zones are Secondary (AXFR from 01) │
└──────────────────────────────────────────────────────────┘
How clustering works
Technitium uses standard DNS zone transfers (AXFR/IXFR) for primary/secondary replication, not a proprietary protocol:
- Primary (01) holds all zones as authoritative primary zones.
- Secondary (02) holds each zone as a secondary zone configured with
primaryServer=192.168.3.252:5300. - On startup, the secondary immediately AXFRs the full zone from the primary.
- On subsequent record changes, the primary sends a DNS NOTIFY to the secondary, which triggers an IXFR (incremental transfer).
- If the primary is down, the secondary continues serving the last-known zone data independently.
Credentials and 2FA
The production auth.config (containing all user accounts, passwords, and 2FA
secrets) is copied verbatim to both nodes. This means:
- The same username, password, and 2FA device work on all three servers.
- The web console is at
http://<host>:5380/on each node. - No credential changes are needed.
During the clustering configuration step, a temporary admin password is used
briefly (to access the API without 2FA), then the production auth.config is
restored. See "Security notes" below.
Prerequisites
- SSH key access to all hosts as
localuserwith passwordless sudo. - The
remote-dns.shwrapper must be able to reach all hosts via Tailscale FQDN. - Docker + Docker Compose on netinfra-01/02 (already installed).
- The production Technitium on tailscale-router must be running.
Usage
cd dns-cluster-setup/
# Step-by-step (recommended for first run):
./setup.sh export # 1. Export config from tailscale-router (READ-ONLY)
./setup.sh deploy01 # 2. Deploy to netinfra-01 as primary
./setup.sh deploy02 # 3. Deploy to netinfra-02 as secondary clone
./setup.sh cluster # 4. Configure clustering (01→02 zone transfers)
./setup.sh verify # 5. Run all verification tests
# Or all at once:
./setup.sh all
Configuration overrides
All defaults can be overridden via environment variables:
| Variable | Default | Description |
|---|---|---|
PRIMARY_IP |
192.168.3.252 |
netinfra-01 LAN IP |
SECONDARY_IP |
192.168.3.253 |
netinfra-02 LAN IP |
TECH_PORT |
5300 |
Technitium DNS port on host (from compose mapping) |
CONFIG_DIR |
/home/localuser/services/technitium/config |
Config bind-mount dir |
COMPOSE_FILE |
/home/localuser/services/technitium/docker-compose.yml |
Compose file |
TEMP_ADMIN_PW |
KnelClusterSetup!2026 |
Temp admin password (used only during clustering, then discarded) |
Scripts
| Script | Purpose |
|---|---|
remote-dns.sh |
SSH/SCP chokepoint for all DNS host access (tsrouter, netinfra01, netinfra02, netboot, sandbox) |
setup.sh |
Master orchestrator: export → deploy → cluster → verify |
verify.sh |
Comprehensive 10-section verification suite |
discover*.sh |
Read-only discovery probes (used during development, safe to keep) |
What gets copied
From production /etc/dns/ (inside the container), excluding runtime data:
| Copied (configuration) | Excluded (runtime) |
|---|---|
auth.config (users, passwords, 2FA) |
cache.bin (DNS cache) |
dns.config (server settings) |
stats/ (query statistics) |
webservice.config (web console) |
logs/ (log files) |
allowed.config (zone transfer ACL) |
|
blocked.config (blocked domains) |
|
blocklist.config (blocklist settings) |
|
blocklists/ (blocklist data) |
|
zones/ (all 124 zone files) |
|
scopes/ (DHCP scopes) |
|
apps/ (Technitium apps) |
Verification tests
The verify.sh script runs 10 categories of tests:
- Container health — both Technitium containers are Up
- API responds — web console API is reachable on both nodes
- Zone count — primary matches production; secondary matches primary
- Forward DNS — known knel.net records resolve identically on both nodes
- External DNS — both nodes can resolve external domains (github.com)
- Zone transfer (AXFR) — secondary can AXFR knel.net from primary
- Reverse DNS — PTR zones have SOA records on both nodes
- Production untouched — container still running, zone count unchanged
- Failover — secondary serves SOA independently (no primary dependency)
- Credentials —
auth.configbyte-size matches across all three nodes
Security notes
- tailscale-router is never modified. The only operation is
docker cp(read) to export the config. No writes, no restarts, no config changes. - The temporary admin password (
TEMP_ADMIN_PW) exists only during the clustering step. After configuration, the productionauth.config(with 2FA) is restored. The temp password is never persisted. - The export tarball (
.export/technitium-production-config.tar.gz) contains production credentials. It is in.gitignoreand should be deleted after setup:rm -rf dns-cluster-setup/.export/ - Each node's existing config is backed up to
config.backup-<timestamp>before replacement, so the change is reversible.
Recovery
If something goes wrong, each node has a backup:
# On netinfra-01 or netinfra-02:
cd /home/localuser/services/technitium/
docker compose down
mv config config.failed
mv config.backup-<timestamp> config
docker compose up -d
Validation on sandbox
After cluster setup, validate that client hosts use the pair correctly:
# From sectestbed-sandbox (or any client):
# Query primary directly:
dig @192.168.3.252 pfv-netinfra-01.knel.net
# Query secondary directly:
dig @192.168.3.253 pfv-netinfra-01.knel.net
# Both should return the same answer.
The KNELServerBuild provisioning code (provisioning/ConfigFiles/NTP/ntp.conf
and provisioning/ConfigFiles/Resolv/resolv.conf) points clients at both
servers for DNS and NTP redundancy. See docs/tailscale.md for the
full DNS architecture analysis.