Files
AZ-NIX-CLIENTS/roles/thin-client/README.md
T
m3ta-chiron 0cc0b5064d fix(thin-client): deep-build fixes + Snipe-IT/Alloy real impl
Tiefen-Validierung via 'nix build .#nixosConfigurations.AZ-TC-NN.config.
system.build.toplevel' hat mehrere reale Bugs gefunden, die 'nix flake
check' nicht sah. Alle drei Pilot-Hosts bauen jetzt sauber durch.

Gefixst:
- freerdp3 → freerdp (umbenannt in nixpkgs-unstable)
- SDDM Theme.Logo will INI-Atom (string), nicht Nix-path → '${path}'
- security.pam.mount.extraVolumes will list-of-string, nicht ein String
- sudoers: 'domain admins' muss als 'domain\ admins' escaped werden
- agenix file-Pfade: ../../secrets/ → ../../../secrets/ (Tiefe korrigiert)
- snapper: config.services.snapper.package gibt es nicht → pkgs.snapper
- samba4Full entfernt (blockiert durch ceph-common python metadata issue
  in nixpkgs-unstable; Thin Clients brauchen es nicht — cifs-utils reicht)
- corp-wifi-ca.pem durch gültiges Dummy-PEM ersetzt (openssl-generiert,
  mit明显 REPLACE-MARKER; build kann PEM parsen)

Functional gemacht:
- Alloy: echtes River-Config mit loki.source.journal + loki.write statt
  barem logging-stub. Journal-Logs mit host/unit/severity-Labels nach
  Loki.
- Snipe-IT: echte Check-in-Logik via curl + jq. Lookup by asset_tag,
  PATCH falls exists, POST falls neu. startAt täglich 03:30. API-Token
  via agenix (snipeit-api-token.age).
- node_exporter push: realer curl-Push alle 60s mit retry on failure.

Safety:
- Placeholder-Assertions in roles/thin-client/default.nix: build schlägt
  fehl, wenn wifi.ssid/hotline/company noch Placeholder sind (außer
  site='staging'). Pilot-Hosts haben site='staging' bis echte Werte da.
- assets/corp-wifi-ca.pem hat deutlich sichtbaren REPLACE-Hinweis.

Neue Options:
- az.tc.monitoring.snipeItUrl (default: snipeit.az-group.local)

Neue Secrets (Placeholder .age-Files zum Ausfüllen):
- snipeit-api-token.age (fleet-wide shared)
2026-07-29 09:06:12 +02:00

9.3 KiB

roles/thin-client — AZ-NIX-CLIENTS Thin Client Fleet

NixOS role for the AZ-TC-NN fleet. Replaces a Windows 10 workstation with a locked-down NixOS + KDE Plasma client that authenticates against Active Directory and provides pre-configured RDP, browser, and Office-Web access.

This document describes the provisioning workflow for a new Fleet Host.

Decision summary

All 20 design decisions from the grilling session are captured in the top-level default.nix comments and the individual submodule headers. Quick reference:

Area Decision
Hardware Dell OptiPlex Micro (3020/3040/5040/7040) + generic-x86_64-uefi fallback
Deployment Stock Linux ISO + nixos-anywhere
Disk BTRFS (@root, @home, @nix, @persist, @snapshots), zstd
AD Realm AZ-GROUP / DNS az-group.local, pre-created computer accounts + keytab via agenix
Shell bash for domain users
Session KDE Plasma 6, Wayland, SDDM, no autologin
Sudo %domain admins with password; local sascha.koenig + jannik.mueller as break-glass
WiFi 802.1X EAP-TLS per-machine, NetworkManager + wpa_supplicant (not iwd)
SMB pam_mount, Kerberos, DFS namespace, lax offline
RDP System-wide Remmina, Kerberos SSO, fullscreen multi-monitor, audio local only
Office 5 .desktop shortcuts via chromium --app=URL; Outlook + Teams autostart
Chromium ManagedBookmarks, sync disabled, uBlock Origin + Bitwarden, no local passwords
RustDesk Client + daemon, self-hosted server, pre-shared key, Wayland portal pre-authorized
OBS Pre-configured "AZ-Default" profile, output to ~/Videos/OBS/
NetBird Per-host setup key, corp DNS + NetBird DNS parallel, accept-routes
Printers Single Pull-Print queue, direct IPPS, Avahi off
Branding Light: corporate wallpaper + SDDM logo + property footer, no banner
Updates system.autoUpgrade daily at 03:00, reboot window 03:00-05:00
Monitoring node_exporter → Pushgateway, Alloy → Loki (journal), Snipe-IT asset check-in (daily 03:30)
Rollout Pilot: 3 hosts, 2 weeks; then 5 → 10 → rest

Provisioning workflow for a new Fleet Host

Assuming you've copied hosts/AZ-TC-01/default.nix to hosts/AZ-TC-NN/default.nix, updated networking.hostName, added the flake entry, and committed.

Step 1 — Pre-create the AD computer account + keytab

Run from a host that can reach the AD DC (e.g. AZ-LT-NIX):

# Pre-create the computer object in the ThinClients OU
adcli precreate --computer-name=AZ-TC-01\$ \
  --domain=az-group.local \
  --host-fqdn=AZ-TC-01.az-group.local \
  --login-type=computer \
  --os-name="NixOS" --os-version="25.11" \
  --domain-ou="OU=ThinClients,DC=az-group,DC=local" \
  administrator@AZ-GROUP

# Join offline and produce a keytab (no real join happens)
adcli join --computer-name=AZ-TC-01\$ \
  --domain=az-group.local \
  --host-fqdn=AZ-TC-01.az-group.local \
  --login-type=computer \
  --user=administrator --verbose \
  -K /tmp/AZ-TC-01.keytab

# Provision the agenix secret
agenix -e secrets/AZ-TC-01-krb5-keytab.age
# In the editor: paste /tmp/AZ-TC-01.keytab contents, save, exit.
rm /tmp/AZ-TC-01.keytab

Step 2 — Provision NetBird setup key

In the NetBird UI (https://netbird.az-group.local):

  1. Go to Setup KeysGenerate new key.
  2. Name: AZ-TC-01, Type: one-time (or reusable for fleet), Expires: 90d.
  3. Copy the key value.
  4. agenix -e secrets/AZ-TC-01-netbird-setupkey.age → paste the key.

Step 3 — Provision WiFi client certificate

In AD CS (https://certsrv.az-group.local/certsrv):

  1. Request a CertificateAdvanced certificate request.

  2. Template: Workstation Authentication (or your corp 802.1X template).

  3. Subject: CN=AZ-TC-01$.

  4. Export the cert + private key as PKCS#12 (.pfx), with exportable key.

  5. Convert to combined PEM (cert + key in one file):

    openssl pkcs12 -in AZ-TC-01.pfx -out AZ-TC-01.pem -nodes
    
  6. agenix -e secrets/AZ-TC-01-wifi-client-cert.age → paste the PEM.

Step 4 — Provision RustDesk credentials

# Generate a strong permanent password per host
PW=$(openssl rand -base64 24)
echo -n "$PW" > /tmp/AZ-TC-01-rustdesk-pw.txt
agenix -e secrets/AZ-TC-01-rustdesk-password.age
# Paste /tmp/AZ-TC-01-rustdesk-pw.txt contents.
rm /tmp/AZ-TC-01-rustdesk-pw.txt

The shared rustdesk-psk.age is one-time setup, reused for all hosts.

Step 5 — Provision the host

Boot the OptiPlex from a stock Linux ISO (any modern NixOS installer ISO or Ubuntu live ISO works). From the admin workstation:

# Bootstrap nixos-anywhere into the booted ISO (via SSH or physical console)
# Then run the install:
nixos-anywhere \
  --flake .#AZ-TC-01 \
  --disko-config ./roles/thin-client/deployment/disko.nix \
  root@<target-ip>

The host will:

  1. Partition the disk via disko (BTRFS layout).
  2. Install NixOS from the flake.
  3. Reboot into the new system.
  4. On first boot: agenix decrypts secrets, sssd starts with keytab, NetBird enrolls via setup key, WiFi connects via EAP-TLS.

Step 6 — Capture the host's SSH host key

After first boot:

ssh-keyscan -t ed25519 AZ-TC-01.netbird | awk '{print $2 " " $3}'

Update secrets.nix with the real AZ-TC-01 SSH key value (replacing the PLACEHOLDER line). Rebuild and redeploy.

Option reference

All tunable parameters live under az.tc.*:

Option Default Description
az.tc.enable false Activate the thin-client role
az.tc.hardwareClass generic-x86_64-uefi One of dell-optiplex-micro or generic-x86_64-uefi
az.tc.site default Site identifier (informational)
az.tc.ad.ou OU=ThinClients,DC=az-group,DC=local AD Organizational Unit for computer objects
az.tc.wifi.ssid AZ-CORP Corp WiFi SSID
az.tc.wifi.caCert ./assets/corp-wifi-ca.pem Corp CA cert (NOT secret)
az.tc.netbird.managementUrl https://netbird.az-group.local:443 NetBird server URL
az.tc.smb.dfsNamespace \\\\az-group.local\\dfs DFS namespace root
az.tc.smb.userShare users/%u Per-user share path
az.tc.smb.commonShares [public software] Common share names
az.tc.smb.mountRoot /mnt/az-dfs Local mount root
az.tc.printing.pullPrintEndpoint ipps://pull-print.az-group.local:443/ipp/print Pull-Print IPPS URI
az.tc.printing.queueName Pull-Print Local CUPS queue name
az.tc.rdp.servers [{name="TS Berlin"; fqdn="ts-berlin.az-group.local";}] TS endpoints
az.tc.chromium.homepage https://www.office.com Browser homepage
az.tc.chromium.bookmarks [Outlook, Teams, SharePoint, Office] Managed bookmarks
az.tc.chromium.bitwardenServerUrl https://vault.bitwarden.com Bitwarden URL
az.tc.rustdesk.serverHost rustdesk.az-group.local RustDesk server host
az.tc.branding.hotline +49 30 1234567 IT hotline for footer
az.tc.branding.wallpaper ./assets/wallpaper.svg Wallpaper path
az.tc.branding.logo ./assets/logo.svg Logo path
az.tc.branding.company AzIntec GmbH Company name
az.tc.monitoring.prometheusPushGateway pushgateway.az-group.local:9091 Pushgateway URL
az.tc.monitoring.lokiUrl http://loki.az-group.local:3100 Loki URL
az.tc.monitoring.snipeItUrl https://snipeit.az-group.local Snipe-IT base URL
az.tc.monitoring.assetTool snipe-it Asset tool integration
az.tc.deployment.diskDevice /dev/sda Disko target disk
az.tc.deployment.swapSizeGB 4 Swap size in GB

Open items before pilot deploy

These placeholders need real values before pilot deploy. They're flagged with # TODO: in the respective modules.

  • Real corp WiFi SSID
  • Real CA root cert (replace corp-wifi-ca.pem)
  • Real DFS namespace path
  • Real common share names
  • Real Terminal Server endpoints
  • Real Chromium bookmarks (Intranet, Helpdesk, ERP, HR)
  • Real Bitwarden server URL (if self-hosted)
  • Real RustDesk server hostname
  • Real Pull-Print IPPS endpoint
  • Real NetBird management URL
  • Real Prometheus Pushgateway URL
  • Real Loki URL
  • Real IT-Hotline phone number
  • Real corporate wallpaper asset (replace placeholder SVG)
  • Real corporate logo asset (replace placeholder SVG)
  • Real AD DCs (az-dc01.az-group.local, az-dc02.az-group.local — currently guessed)
  • Real AD Computer OU DN

Build validation

To validate the config without deploying:

# Eval-check all hosts
nix flake check

# Build a pilot host closure (full closure, takes ~20 min uncached)
nix build --no-link .#nixosConfigurations.AZ-TC-01.config.system.build.toplevel

# Try a VM boot
nix run .#nixosConfigurations.AZ-TC-01.config.system.build.vm

All three pilot hosts pass both nix flake check and full closure build.

Placeholder assertions

The role ships with placeholder assertions that fire at build time if operator-relevant values are still defaults. To silence for lab/staging hosts, set az.tc.site = "staging" (see hosts/AZ-TC-01/default.nix).

Currently asserted:

  • az.tc.wifi.ssid != "AZ-CORP" — real Corp SSID must be set
  • az.tc.branding.hotline != "+49 30 1234567" — real hotline must be set
  • az.tc.branding.company != "AzIntec GmbH" — real company name must be set

Add more assertions as customer-specific values firm up.