# 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`:
```yaml
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`:
```yaml
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

```bash
# 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.

```bash
./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`](pfv-netboot-setup.md) | reference-node documentation |
| [`pfv-netinfra-setup.md`](pfv-netinfra-setup.md) | this document |
