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

137 lines
7 KiB
Markdown

# 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.