Files
PFVCluster/netinfra/pfv-netinfra-setup.md
T
mrcharles a7fbad60ab docs: end-to-end gardening — links, stale refs, tailscale.md update
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>
2026-07-28 11:28:52 -05:00

9.2 KiB

pfv-netinfra-01 / pfv-netinfra-02 — Network Services Setup

These two nodes replicate the network-infrastructure services of pfv-netboot (Pi-hole, Technitium DNS, NTP). They were deployed by setup-netinfra.sh, which reads config from pfv-netboot (read-only) and relays it to each target.

1. Nodes

pfv-netinfra-01 pfv-netinfra-02
OS Debian 13 (trixie), kernel 6.12.96+deb13 Debian 13 (trixie)
LAN ens18 192.168.3.252/24 ens18 192.168.3.253/24
Tailscale 100.70.181.72 100.93.194.82
RAM / Disk 1.9 GiB / 30 GB (27 GB free) 3.7 GiB / 30 GB (27 GB free)
Resolver Tailscale MagicDNS (100.100.100.100) same
Docker 29.6.2 (pre-installed, enabled) 29.6.2
Access ssh localuser@pfv-netinfra-0X, passwordless sudo; localuser not in docker group → use sudo docker same

2. Service layout

All services live under /home/localuser/services/<svc>/ (owned by localuser so the compose files are directly editable; data dirs keep container uids):

/home/localuser/services/
├── pihole/
│   ├── docker-compose.yml
│   └── etc-pihole/          # copied from netboot /root/pihole/etc-pihole
│       ├── pihole.toml      # Pi-hole v6 config (upstreams, etc.)
│       ├── gravity.db       # adlists / domainlists / clients / groups
│       ├── adlists.list
│       ├── dnsmasq.conf
│       ├── tls.{crt,pem,crt_ca}
│       └── versions
├── ntp/
│   └── docker-compose.yml   # chrony container (see §5 — not used; host ntpsec serves)
└── technitium/
    ├── docker-compose.yml
    └── config/              # copied from netboot orphaned volume dns_tsys-dns-config/_data
        ├── dns.config
        ├── auth.config
        ├── scopes/Default.scope
        ├── self-signed-cert.pfx
        └── zones/           # knel.net.zone + 12 Tailscale reverse zones

3. Pi-hole (container pihole)

Image pihole/pihole:latest; restart: always; cap_add: [SYS_NICE].

Host port Container Purpose
53/tcp, 53/udp 53 DNS (the LAN/Tailscale recursive resolver)
10002/tcp 80 Web admin (HTTP)
10003/tcp 443 Web admin (HTTPS)

docker-compose.yml:

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    hostname: pihole
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "10002:80/tcp"
      - "10003:443/tcp"
    environment:
      TZ: 'America/Chicago'
      FTLCONF_webserver_api_password: 'REDACTED_PASSWORD'
      FTLCONF_dns_listeningMode: 'all'
    volumes:
      - './etc-pihole:/etc/pihole'
    cap_add:
      - SYS_NICE
    restart: always
  • Upstream DNS (from copied pihole.toml): 192.168.3.16, 8.8.8.8, 2001:4860:4860::8888.
  • Adlist: https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts.
  • pihole.toml interface was adapted from netboot's eth0 to the target's ens18.
  • Web admin: http://<node>:10002/admin/ — password REDACTED_PASSWORD (same as netboot).
  • Web UI URL per node: http://100.70.181.72:10002/admin/ (-01), http://100.93.194.82:10002/admin/ (-02).

4. Technitium DNS (container tsys-dns)

Image technitium/dns-server; restart: always. Authoritative DNS for knel.net (and Tailscale reverse zones), config copied verbatim from netboot's orphaned dns_tsys-dns-config volume.

Host port Container Purpose
5300/tcp, 5300/udp 53 DNS (remapped — see note)
5380/tcp 5380 Web console (HTTP)
53443/tcp 53443 Web console (HTTPS)

docker-compose.yml:

services:
  technitium:
    image: technitium/dns-server
    container_name: tsys-dns
    ports:
      - "5300:53/tcp"
      - "5300:53/udp"
      - "5380:5380/tcp"
      - "53443:53443/tcp"
    volumes:
      - './config:/etc/dns'
    restart: always
  • Zones loaded (verified): knel.net SOA → dns.knel.net. hostadmin.knel.net. 2025062313 900 300 604800 900, plus 12 Tailscale reverse zones.
  • Web console: http://<node>:5380/ → user admin + the original Technitium password (carried over via auth.config). If the password is unknown, reset it from the console or by removing config/auth.config and recreating the container.
  • Port note: Technitium's native DNS port (53) is remapped to host 5300 because Pi-hole already owns host :53 (they cannot both bind 0.0.0.0:53). To query the authoritative server: dig -p 5300 @<node> knel.net SOA. To make Pi-hole resolve knel.net via Technitium, add a conditional/local upstream in Pi-hole pointing to the container (e.g. 127.0.0.1#5300 is not host-reachable from Pi-hole's netns — use the docker bridge IP of tsys-dns, or add knel.net A-records directly in Pi-hole's Local DNS).

5. NTP (host ntpsec, not a container)

Both targets already run a bare-metal ntpsec daemon (active, enabled) that serves NTP on every local address — including the Tailscale IP — and keeps the system clock synced. This is the same daemon family as netboot's own bare-metal ntpsec.

  • Why no chrony container? netboot's chrony container (tsys-ntp) binds the Tailscale IP 100.103.64.82:123; on netboot that works only because its ntpsec does not pre-bind the specific Tailscale-IP socket. On these targets ntpsec does bind the Tailscale IP, so the container cannot claim it (address already in use) and would be a non-functional duplicate (verified: the container started but never synced — Stratum 0). It is therefore intentionally omitted; host ntpsec provides NTP. setup-netinfra.sh detects an active host NTP unit and removes any stale tsys-ntp container.
  • ntpsec config (/etc/ntpsec/ntp.conf): Debian NTP pool (0-3.debian.pool.ntp.org), restrict default kod nomodify noquery limited (serves time, blocks mgmt queries).
  • Verified sync: -01 stratum 2 (~2 ms offset), -02 stratum 3 (~0.2 ms offset), leap normal.

The ntp/docker-compose.yml is still written on each node for parity/reference (and in case the host NTP is ever disabled — then sudo docker compose -f /home/localuser/services/ntp/docker-compose.yml up -d brings up chrony).

6. Verification results (2026-07-28)

Check pfv-netinfra-01 pfv-netinfra-02
pihole health healthy healthy
dig @127.0.0.1:53 pi.hole 172.18.0.2 172.18.0.2
Pi-hole web :10002 HTTP 302 (→login) HTTP 302
dig @127.0.0.1:5300 knel.net SOA SOA answered SOA answered
Technitium web :5380 HTTP 200 HTTP 200
NTP daemon ntpsec, stratum 2, synced ntpsec, stratum 3, synced

7. Operating the services

# status
sudo docker ps

# Pi-hole
sudo docker compose -f /home/localuser/services/pihole/docker-compose.yml ps
sudo docker compose -f /home/localuser/services/pihole/docker-compose.yml logs -f
sudo docker exec pihole pihole -v          # version
sudo docker exec pihole pihole -g          # rebuild gravity
sudo docker exec pihole pihole -a -p       # set/change web password

# Technitium
sudo docker compose -f /home/localuser/services/technitium/docker-compose.yml logs -f
sudo docker exec tsys-dns sh               # explore /etc/dns

# NTP (host)
systemctl status ntpsec
ntpq -pn

8. Differences from pfv-netboot (intentional)

  1. Layout under /home/localuser/services/ instead of /root (so localuser can manage compose files); Pi-hole data dir still owned by localuser, as on netboot.
  2. Pi-hole interface set to ens18 (targets' NIC) instead of netboot's eth0.
  3. NTP: host ntpsec (Debian pool) used instead of netboot's chrony container (the container cannot bind the Tailscale IP here; see §5).
  4. Technitium DNS host port remapped 53 → 5300 to avoid clashing with Pi-hole on :53. The knel.net zone and all reverse zones are identical to netboot's.
  5. Pi-hole query logs (pihole-FTL.db*) and regenerable caches/backups are not copied (transient); gravity DB and all configuration are.

9. Re-running / reproducing

setup-netinfra.sh is idempotent — it skips re-copying config if already present and uses docker compose up -d (no-ops when unchanged). It reads pfv-netboot read-only and never mutates it.

./setup-netinfra.sh                       # deploy to both nodes
./setup-netinfra.sh pfv-netinfra-01       # deploy one node
./setup-netinfra.sh pfv-netinfra-01 verify   # verify only

Prerequisites: SSH key access to all three hosts as localuser with passwordless sudo; the targets reach 192.168.3.16/8.8.8.8 for Pi-hole upstream and the internet for image pulls.

10. Files in this directory

File Purpose
setup-netinfra.sh orchestrator: deploys + verifies the clone on -01/-02
audit-netboot.sh broad read-only audit of pfv-netboot
deep-audit-netboot.sh docker inspect / compose / volume deep audit (read-only)
gather-configs.sh targeted config pull (pihole.toml, technitium) (read-only)
baseline.sh read-only baseline of a target node
netboot-audit.txt, netboot-deep-audit.txt, netboot-configs.txt audit output
pfv-netboot-setup.md reference-node documentation
pfv-netinfra-setup.md this document