This repository has been archived on 2026-09-17. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
nixos-config/docs/pinix-tailnet-dns.md

12 KiB

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.

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.