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-identity-stack.md

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.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):

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.