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.
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 IDecfe4bc08f65904f34cf0c4631d4990b.head.lukasrosier.com: record IDf502f15c92924d4707efbdbaf7bdf456.lldap.lukasrosier.com: record IDce66f3caba1101c839c4985be413fdb1.
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 oftnix, removal of the orphanend0masquerade rule); seedocs/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, andbindare active; no failed units. pinix-dns-netns,tailscaled-pinix, andunbound-pinixare not found. Neither a pinix network namespace norpdns-hostexists. 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/10andfd7a:115c:a1e0::/48with three nodes:tnix-pinixnet(.1, offline),localhost(.2, online), andtnix(.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.3resolve both identity names to100.64.0.3. Queries to LAN192.168.50.1return their public CNAMEs toipv6-test.0ff.space. TLS-verified Dex discovery over the old-tailnet address passes. - On pinix, UDP/TCP queries to
127.0.0.2synthesize the expected64:ff9b::c000:aaand64:ff9b::c000:abforipv4only.arpa. BIND at127.0.0.1serves the Dex CNAME authoritatively. Old Unbound responds over IPv4 and IPv6 transport;headAAAA returns NOERROR/NODATA because its local override defines only an A record. - Original pinix Tailscale is Running/online at
100.64.0.3andfd7a:115c:a1e0::3, controlled byhttps://head.0ff.space. Both tnix Tailscale services andtnix-pinixnet-netnsare active. Inside that namespace.3is 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,unboundandbindunits/configuration dependencies: passed. sudo -n bash scripts/test-dex-ldap-env.shon 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-checkconfon the generated candidate, changing only its absent runtime directory to/tmpin a temporary copy: passed. The actual systemd runtime directory and namespace still need post-activation tests.