137 lines
7 KiB
Markdown
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.
|