# Pinix public and private DNS bootstrap ## Boundaries | Component | Address / name | Role | | --- | --- | --- | | Cloudflare | `dex.lukasrosier.com`, `head.lukasrosier.com`, `lldap.lukasrosier.com` | DNS-only CNAMEs to `ipv6-test.0ff.space`; public bootstrap | | BIND on pinix | existing loopback / LAN IPv6 listeners | ACME challenge zone and candidate public zone; no recursive service | | Existing Unbound | pinix `100.96.0.4`, `fd7a:115c:a1e0:96::4` | Resolver for the shared pinix Tailnet | | New Unbound | new tailnet `100.96.0.1`, `fd7a:115c:a1e0:96::1` | Private DNS inside network namespace `pinix-dns` | | Headscale MagicDNS | `hn.lukasrosier.com` | New tailnet device names | The main nodes currently use one Headscale user, `lukas`: | Node | IPv4 | Headscale user | | --- | --- | --- | | `pinix` | `100.96.0.4` | `lukas` | | `tnix` | `100.96.0.5` | `lukas` | | `blunix` | `100.96.0.6` | `lukas` | | `pinix-dns` | `100.96.0.1` | `pinix-dns` (technical) | The iPhone is registered as `localhost-1` under `lukas` at `100.96.0.2`. The former `localhost` node under `admin` was removed after the migration. The new private service zone is `int.lukasrosier.com`. Initially only `dns.int.lukasrosier.com` exists. Unknown names in this zone return NXDOMAIN locally instead of falling through to the public wildcard on blunix. Other private services must be added with their actual **new** tailnet addresses and appropriate listener/firewall/access policy. The DNS node is not a subnet router, exit node or proxy for the old tailnet. Since 2026-09-14 it forwards exactly two ports from its own tailnet address to the host: TCP 443 to Traefik `web-internal` on `[fd42:7069:6e69:78::1]:8443` and TCP 22 to sshd (for `git@`). The first such service is `git.lukasrosier.com` (AAAA only, `hosts/pinix/localServices/gitserver/`, `docs/intent/gitserver-pinix.md`). The existing `*.lukasrosier.com` certificate does not cover `*.int.lukasrosier.com`; HTTPS services in this subdomain will need their own certificate coverage. ## Isolation and IPv6-only uplink `tailnet-dns.nix` adds `tailscaled-pinix` and `unbound-pinix` in an isolated network namespace, with separate Tailscale state and control socket. The main `tailscaled` service now belongs to the shared pinix Tailnet as the `pinix` node; the technical namespace keeps its separate `pinix-dns` identity. Headscale allocates `100.96.0.0/24` and `fd7a:115c:a1e0:96::/64` (live since 2026-09-14). `headscale nodes backfillips` (0.29.3) does **not** move existing addresses outside these prefixes, it only fills missing families; migrating a node means deleting and re-registering it. These remain subsets of the Tailscale address ranges. The old blunix control plane remains enabled for legacy and recovery nodes. A veth pair uses `fd42:7069:6e69:78::1/64` (host) and `::2` (namespace). Host forwarding permits this namespace's IPv6 egress only through `wan0`, masquerades that single source, and permits only established return traffic. Connections to host TCP 443 allow Headscale/Dex bootstrap without Mango hairpinning. No forwarding to the old tailscale interface is allowed. Inside the namespace, IPv6 forwarding is enabled for the two DNAT'd ports only: FORWARD policy is DROP, SNAT to `::2` keeps host replies off the host's own `fd7a:115c:a1e0::/48` route on `tailscale0`. IPv4 forwarding stays off. UDP 41642 (the node's WireGuard port) is DNAT'd from every local address of pinix into the namespace, like a router port forward, and replies of those flows may leave `pdns-host` towards any interface. Without it the node only appears as pinix's masqueraded public IPv6, packets from the LAN to that address never reached the namespace, and peers stayed on DERP. Measured 2026-09-14 from tnix: `via DERP(fra)` ~100 ms before, `direct` 1 ms after. Host `wan0.accept_ra=2` preserves router advertisements with host forwarding enabled. No static address is added to `wan0`; doing so would disable dhcpcd router solicitation. Host DNS uses the router DNS64 at `127.0.0.2`. The new Unbound answers private records locally and forwards other queries to Telekom's DNS64 resolver. This differs from the existing recursive Unbound: the IPv6-only uplink cannot reliably contact IPv4-only authoritative servers directly. Local validation of upstream-synthesized DNS64 answers is disabled; this instance does not claim independent DNSSEC validation. Headscale distributes this resolver via split DNS only for names it answers itself (currently `git.lukasrosier.com`), preserving normal client DNS for other domains -- a split for all of `lukasrosier.com` would make `head.` and `dex.` unresolvable for clients whenever this resolver is down. The DNS node itself uses `--accept-dns=false`. ## Public records `scripts/configure-pinix-identity-dns.sh` defaults to read-only `--check`. With `--apply` it creates only missing records after preflighting all three names, and refuses to replace conflicting records. Run it with the existing agenix Cloudflare EnvironmentFile using systemd, with curl and jq on PATH; never pass or print the token on the command line. Created on 2026-09-13, TTL 300, `proxied=false`: - `dex.lukasrosier.com`: record ID `ecfe4bc08f65904f34cf0c4631d4990b`. - `head.lukasrosier.com`: record ID `f502f15c92924d4707efbdbaf7bdf456`. - `lldap.lukasrosier.com`: record ID `ce66f3caba1101c839c4985be413fdb1`. Both target `ipv6-test.0ff.space`. The target, wildcard, mail records, delegation and `0ff.space` control-plane records were not changed. Public TLS-verified discovery and health checks passed from blunix. ## Enrolment and validation After activation, run `scripts/enrol-pinix-dns.sh` as root on pinix with headscale, tailscale and jq on PATH. It creates only the dedicated `pinix-dns` machine user if missing and a single-use, ten-minute pre-auth key. The key exists only in a root-only temporary file in `/run`, removed on exit. Persistent reconnects use `/var/lib/tailscale-pinix/tailscaled.state`; no old auth key is reused and no plaintext credential enters the repository. The script verifies both expected node addresses and fails on a mismatch. Do not advertise this resolver until enrolment, TCP/UDP DNS and isolation checks have passed. User OIDC login remains a separate test; machine-key enrolment does not prove LDAP group membership or the login callback. Rollback: activate the previously recorded system generation. That removes the namespace services and scoped forwarding rules while preserving the old tailnet. Keep the new persistent state for recovery. The two public CNAMEs are independent of this namespace and need not be removed to roll back private DNS; any public-DNS rollback should target only the IDs above. ## Read-only audit: 2026-09-13, 18:16 UTC > Historical. Superseded by the activation on 2026-09-14 (prefix change, > enrolment of `pinix-dns`, migration of `tnix`, removal of the orphan > `end0` masquerade rule); see `docs/intent/gitserver-pinix.md`. Checkout: `/home/builder/platform/nixos-config`, HEAD `dc1595a`, including existing uncommitted DNS work. The historical `/home/drone/nixos-config` does not exist on tnix. **The DNS candidate is not ready for activation.** - Pinix running system and persistent profile both point to `/nix/store/pbssq8gmmnv34mjqq1zsw9dp8pha0pfw-nixos-system-pinix-26.11.20260910.8ce4ef6`. `tailscaled`, `unbound`, `pinix-router-dns`, `dex-ldap-env`, `dex`, `headscale`, `lldap`, `traefik`, and `bind` are active; no failed units. - `pinix-dns-netns`, `tailscaled-pinix`, and `unbound-pinix` are not found. Neither a pinix network namespace nor `pdns-host` exists. New private DNS TCP/UDP, private-zone NXDOMAIN and pinix namespace isolation remain untested; they must not be reported as passing. - Live Headscale uses `100.64.0.0/10` and `fd7a:115c:a1e0::/48` with three nodes: `tnix-pinixnet` (.1, offline), `localhost` (.2, online), and `tnix` (.3, online). No dedicated DNS node exists. The candidate prefix change is now a migration. The enrolment helper refuses first-node bootstrap on a populated database before creating users or keys. - Live and evaluated Headscale DNS have `override_local_dns=false`, no global resolvers and an empty split map. The private zone is not yet distributed. Add the scoped resolver only after approved address planning, activation, enrolment and successful private DNS tests. - From tnix's host namespace, UDP and TCP queries to old-tailnet `100.64.0.3` resolve both identity names to `100.64.0.3`. Queries to LAN `192.168.50.1` return their public CNAMEs to `ipv6-test.0ff.space`. TLS-verified Dex discovery over the old-tailnet address passes. - On pinix, UDP/TCP queries to `127.0.0.2` synthesize the expected `64:ff9b::c000:aa` and `64:ff9b::c000:ab` for `ipv4only.arpa`. BIND at `127.0.0.1` serves the Dex CNAME authoritatively. Old Unbound responds over IPv4 and IPv6 transport; `head` AAAA returns NOERROR/NODATA because its local override defines only an A record. - Original pinix Tailscale is Running/online at `100.64.0.3` and `fd7a:115c:a1e0::3`, controlled by `https://head.0ff.space`. Both tnix Tailscale services and `tnix-pinixnet-netns` are active. Inside that namespace `.3` is local and old-pinix HTTPS at that address fails locally; the same request succeeds outside. This verifies that overlap is isolated, not all possible paths. Namespace IPv4/IPv6 forwarding is 1 as declared for SSH DNAT; host IPv6 rules allow its uplink and reject other forwarding. SSH to tnix failed host-key verification; its read-only namespace checks were performed locally with sudo instead. - TLS-verified Dex discovery and Headscale health pass from pinix and publicly from blunix. Issuer: `https://dex.lukasrosier.com/dex`. No actual user login, LDAP credential validation or OIDC callback was performed. Registered nodes alone are not login evidence. ### Known drift and approval boundary Pinix has orphan `pinix-dns-forward` rules and two scoped masquerade rules for `fd42:7069:6e69:78::2/128`, through obsolete `end0` and through `wan0`, although the DNS namespace is absent. The candidate declares only `wan0`; its stop script does not remove the old `end0` rule. The origin of these live imperative rules is not established. No cleanup was performed. After explicit approval, remove the obsolete rule with `ip6tables -t nat -D POSTROUTING -s fd42:7069:6e69:78::2/128 -o end0 -j MASQUERADE`. A full rollback also requires removing both namespace FORWARD jumps, the custom chain and its `wan0` masquerade rule after confirming they remain unused. Alternatively reconcile intended rules through reviewed DNS activation. Neither action is authorized by this audit. Resolve address allocation and migration of the three registrations before any switch. A passing evaluation does not establish deployment readiness. No remote files, services, registrations or firewall rules were changed. No full-system build, dry activation or activation was run. ### Local validation - `nix flake check path:. --no-build`: passed, existing deprecation warnings. - Focused build of `pinix-dns-netns`, `tailscaled-pinix`, `unbound-pinix`, `unbound` and `bind` units/configuration dependencies: passed. - `sudo -n bash scripts/test-dex-ldap-env.sh` on tnix: all three synthetic systemd EnvironmentFile round trips passed; not a user login test. - Shell syntax checks and `git diff --check`: passed. - Mocked populated-database enrolment test: refused before any mutation. - `unbound-checkconf` on the generated candidate, changing only its absent runtime directory to `/tmp` in a temporary copy: passed. The actual systemd runtime directory and namespace still need post-activation tests.