# 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](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`: ```text 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.sh` passed on pinix: three synthetic passwords round-tripped through actual systemd EnvironmentFile parsing, including special characters and embedded newlines; output permissions are 0600. - `git diff --check` and shell syntax checks passed. - `nix flake check path:. --no-build` passed 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 `configtest` reached 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 `--resolve` passed without disabling certificate validation: Dex discovery reports the expected issuer and Headscale `/health` reports `pass`. 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): ```sh 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.