7 KiB
Pinix identity and Headscale stack
Pinix runs a parallel Dex + LLDAP + Headscale stack under
config.domain2 (lukasrosier.com in the checked-in defaults):
Status: deployed on 2026-09-13 with explicit approval. Dex, Headscale,
Traefik and LLDAP are active. TLS-verified OIDC discovery, Headscale health
and the Dex authorization page pass. On 2026-09-14 the main nodes were
migrated to the pinix Headscale under the LLDAP user lukas:
| Node | Tailnet address | Headscale user | Role |
|---|---|---|---|
pinix |
100.96.0.4 |
lukas |
main host |
tnix |
100.96.0.5 |
lukas |
main host |
blunix |
100.96.0.6 |
lukas |
main host |
localhost-1 (iPhone) |
100.96.0.2 |
lukas |
personal client |
pinix-dns |
100.96.0.1 |
pinix-dns |
technical DNS/Git entrypoint |
The iPhone was re-added to the custom Headscale server as lukas; the old
admin node localhost was removed. The technical pinix-dns identity
remains separate because it owns the isolated resolver and service entrypoint.
The three public identity CNAMEs now target ipv6-test.0ff.space; delegation
remains at Cloudflare. Current live DNS findings and migration blockers
are recorded in pinix-tailnet-dns.md.
| Endpoint | Pinix service | Local backend |
|---|---|---|
dex.lukasrosier.com |
Dex | 127.0.0.1:5556 |
head.lukasrosier.com |
Headscale | 127.0.0.1:3001 |
lldap.lukasrosier.com |
LLDAP board | 127.0.0.1:17170, behind Traefik |
Traefik serves the first two endpoints with the existing domain2 wildcard
certificate. Pinix itself maps the names to loopback so the OIDC discovery
request does not hairpin through Mango.
The internal Unbound view resolves dex and head to pinix's address in the
pinix Tailnet, while pinix itself maps the names to loopback so the OIDC
discovery request does not hairpin through Mango. Headscale uses the dedicated
100.96.0.0/24 and fd7a:115c:a1e0:96::/64 ranges. The old blunix
Headscale remains enabled for legacy and recovery nodes, but the three main
hosts above no longer depend on it.
DNS prerequisite
The public lukasrosier.com zone currently remains at Cloudflare. These
three DNS-only CNAMEs are managed by
scripts/configure-pinix-identity-dns.sh:
head.lukasrosier.com CNAME ipv6-test.0ff.space.
dex.lukasrosier.com CNAME ipv6-test.0ff.space.
lldap.lukasrosier.com CNAME ipv6-test.0ff.space.
The target is the existing Mango-updated AAAA record, so the three identity names follow a changing public IPv6 address without duplicating the updater. The same CNAMEs are present in pinix's candidate BIND zone for the later authoritative-DNS step. Do not use the existing wildcard: it still points at blunix.
Current identity and cutover state
The LLDAP user lukas (as.as@mailfence.com) is a member of intern and is
linked to the pinix Headscale OIDC identity. Headscale has
allowed_groups = ["intern"], so only accounts in that group can create
OIDC nodes. The LLDAP admin account remains the directory administration
account; it is not the identity used by the main devices.
The reusable pinix pre-auth key is encrypted in secrets/ and is shared by
the main blunix, tnix and pinix system clients. The technical
pinix-dns node keeps its own machine identity. The temporary second tnix
registration was removed after the main node was migrated.
The old blunix Dex/Headscale modules and head.0ff.space remain in place for
legacy nodes and recovery. They are a separate control plane and are not
bridged with the pinix Tailnet.
The iPhone migration is complete: the Tailscale app uses the custom control
server https://head.lukasrosier.com and the LLDAP account lukas.
The LLDAP board is available at https://lldap.lukasrosier.com both through
the public IPv6 entrypoint and from the pinix Tailnet. It remains bound to
127.0.0.1:17170; Traefik is the only network-facing path. Tailnet DNS sends
the name through the technical pinix-dns node, while public DNS follows the
same dynamic IPv6 target as the Dex and Headscale endpoints.
Validation (2026-09-13)
- Built the Dex, LDAP environment helper, Headscale, Traefik, Unbound and BIND systemd units and their configuration dependencies, without a kernel build. The BIND derivation also validates the updated candidate zone.
scripts/test-dex-ldap-env.shpassed on pinix: three synthetic passwords round-tripped through actual systemd EnvironmentFile parsing, including special characters and embedded newlines; output permissions are 0600.git diff --checkand shell syntax checks passed.nix flake check path:. --no-buildpassed for all declared checks, with existing deprecation warnings (and a recoverable GitHub lookup warning).- Full native pinix build passed, reusing the existing kernel:
/nix/store/ra5qca5ffp4412wdqw4f125yxvcpw6p5-nixos-system-pinix-26.11.20260910.8ce4ef6. - Dry activation passed. A switch also restarts existing BIND, LLDAP, Traefik and Unbound, reloads firewall/DBus, and restarts nix-daemon and journald. The repository's pinned platform is newer than the running generation; deployment therefore affects more than the two new services.
- Before activation, native Headscale
configtestreached OIDC discovery but failed with HTTP 404 because the new Dex endpoint was not deployed. This was not a passing runtime test. Despite its name, configtest initialized Headscale's database and Noise key under/var/lib/headscale; that state has been preserved. On activation, systemd assigned the state to headscale:headscale (directory 0750, Noise key 0600). - Activation passed. The running system and persistent system profile both point to the native build above. All eight checked services are active: dex-ldap-env, dex, headscale, traefik, lldap, bind, unbound and tailscaled. No failed systemd units were reported.
- Headscale retried once while Traefik loaded its certificate, then completed
OIDC discovery and started successfully. HTTPS checks from pinix and over
LAN with curl
--resolvepassed without disabling certificate validation: Dex discovery reports the expected issuer and Headscale/healthreportspass. The configured client's authorization flow reaches an HTTP 200 login page. This does not verify LDAP credentials or the token callback. - Public identity CNAMEs are now present; end-to-end OIDC user login remains unverified in the read-only audit. Existing client enrolments and the old blunix control plane are unchanged.
For focused validation from the repository root (includes untracked files):
nix build --impure --no-link --expr '
let c = (builtins.getFlake ("path:" + toString ./.)).nixosConfigurations.pinix.config;
in map (n: c.systemd.units.${n}.unit) [
"dex.service" "dex-ldap-env.service" "headscale.service"
"traefik.service" "unbound.service" "bind.service"
]'
Inspect a full-system nix build --dry-run on pinix before building it.
Do not start an emulated Raspberry Pi kernel build on tnix for this change.