diff --git a/hosts/AZ-TC-01/default.nix b/hosts/AZ-TC-01/default.nix index c606beb..37e56aa 100644 --- a/hosts/AZ-TC-01/default.nix +++ b/hosts/AZ-TC-01/default.nix @@ -11,6 +11,9 @@ az.tc = { enable = true; hardwareClass = "dell-optiplex-micro"; + # Staging mode silences placeholder-warnings (real SSID, hotline, + # company name) until we have the final corp values. + site = "staging"; }; networking.hostName = "AZ-TC-01"; diff --git a/hosts/AZ-TC-02/default.nix b/hosts/AZ-TC-02/default.nix index 353ec9f..6439f11 100644 --- a/hosts/AZ-TC-02/default.nix +++ b/hosts/AZ-TC-02/default.nix @@ -6,6 +6,7 @@ az.tc = { enable = true; hardwareClass = "dell-optiplex-micro"; + site = "staging"; }; networking.hostName = "AZ-TC-02"; diff --git a/hosts/AZ-TC-03/default.nix b/hosts/AZ-TC-03/default.nix index ca5947a..9e3b975 100644 --- a/hosts/AZ-TC-03/default.nix +++ b/hosts/AZ-TC-03/default.nix @@ -6,6 +6,7 @@ az.tc = { enable = true; hardwareClass = "dell-optiplex-micro"; + site = "staging"; }; networking.hostName = "AZ-TC-03"; diff --git a/roles/thin-client/README.md b/roles/thin-client/README.md index 31feb23..b63c9fc 100644 --- a/roles/thin-client/README.md +++ b/roles/thin-client/README.md @@ -33,7 +33,7 @@ Quick reference: | 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, promtail → Loki, Snipe-IT asset check-in | +| 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 @@ -171,6 +171,7 @@ All tunable parameters live under `az.tc.*`: | `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 | @@ -206,9 +207,25 @@ To validate the config without deploying: # Eval-check all hosts nix flake check -# Build a pilot host closure -nix build .#nixosConfigurations.AZ-TC-01.config.system.build.toplevel +# 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. diff --git a/roles/thin-client/apps/remmina.nix b/roles/thin-client/apps/remmina.nix index d012b53..b587671 100644 --- a/roles/thin-client/apps/remmina.nix +++ b/roles/thin-client/apps/remmina.nix @@ -78,7 +78,7 @@ in { config = mkIf cfg.enable { environment.systemPackages = with pkgs; [ remmina - freerdp3 + freerdp ]; # System-wide Remmina profiles diff --git a/roles/thin-client/apps/rustdesk.nix b/roles/thin-client/apps/rustdesk.nix index 377599c..9c98a41 100644 --- a/roles/thin-client/apps/rustdesk.nix +++ b/roles/thin-client/apps/rustdesk.nix @@ -67,14 +67,14 @@ in { # Shared pre-shared key (one for all thin clients) age.secrets."rustdesk-psk" = { - file = ../../secrets/rustdesk-psk.age; + file = ../../../secrets/rustdesk-psk.age; mode = "0400"; owner = "root"; }; # Per-host permanent password age.secrets."${hostname}-rustdesk-password" = { - file = ../../secrets/${hostname}-rustdesk-password.age; + file = ../../../secrets/${hostname}-rustdesk-password.age; mode = "0400"; owner = "root"; }; diff --git a/roles/thin-client/default.nix b/roles/thin-client/default.nix index 89ffc47..5f16f6a 100644 --- a/roles/thin-client/default.nix +++ b/roles/thin-client/default.nix @@ -104,11 +104,31 @@ in { # Firewall is enabled by the network module. # ── assertions / warnings ──────────────────────────────────────── + # Catches un-overridden placeholders at build time so the operator + # notices BEFORE deploying to a real Thin Client. Each assertion + # checks a "placeholder" marker value; override the option in + # hosts/AZ-TC-NN/default.nix to silence. assertions = [ { assertion = cfg.enable -> (config.networking.hostName or "") != ""; message = "az.tc.enable requires networking.hostName to be set (e.g. 'AZ-TC-01')."; } + { + assertion = cfg.enable -> config.az.tc.wifi.ssid != "AZ-CORP" || config.az.tc.site == "staging"; + message = '' + az.tc.wifi.ssid is still the placeholder 'AZ-CORP'. Set the + real corp WiFi SSID in hosts/AZ-TC-NN/default.nix (or set + az.tc.site = "staging" to silence for lab/test hosts). + ''; + } + { + assertion = cfg.enable -> config.az.tc.branding.hotline != "+49 30 1234567" || config.az.tc.site == "staging"; + message = "az.tc.branding.hotline is still the placeholder — set the real IT-Hotline number."; + } + { + assertion = cfg.enable -> config.az.tc.branding.company != "AzIntec GmbH" || config.az.tc.site == "staging"; + message = "az.tc.branding.company is still the placeholder 'AzIntec GmbH' — set the real company name."; + } ]; }; } diff --git a/roles/thin-client/deployment/snapper.nix b/roles/thin-client/deployment/snapper.nix index 4ed6b9d..d4ea16b 100644 --- a/roles/thin-client/deployment/snapper.nix +++ b/roles/thin-client/deployment/snapper.nix @@ -4,7 +4,7 @@ # before and after each `nixos-rebuild switch`, plus hourly/daily. # # Q18 decision: Generation-Picker + BTRFS-Snapshot. -{config, lib, ...}: let +{config, lib, pkgs, ...}: let inherit (lib) mkIf; cfg = config.az.tc; in { @@ -31,7 +31,7 @@ in { deps = []; text = '' if [ -d /.snapshots ]; then - ${config.services.snapper.package}/bin/snapper -c root create \ + ${pkgs.snapper}/bin/snapper -c root create \ -d "pre-switch $(date +%Y-%m-%d-%H%M)" || true fi ''; diff --git a/roles/thin-client/identity/ad.nix b/roles/thin-client/identity/ad.nix index cac9105..4272ff8 100644 --- a/roles/thin-client/identity/ad.nix +++ b/roles/thin-client/identity/ad.nix @@ -53,12 +53,16 @@ in { config = lib.mkIf cfg.enable { environment.systemPackages = with pkgs; [ - adcli - sssd - samba4Full - krb5 - realmd - oddjob + adcli # AD computer-account precreate + join helper + sssd # AD client daemon + krb5 # MIT Kerberos client + realmd # DBus service for realm discovery (used by adcli wrapper) + # NOTE: samba4Full intentionally NOT included — it would pull in + # ceph-common which is currently broken in nixpkgs-unstable + # (python metadata issue). Thin Clients don't need Samba server + # or smbclient — share mounting uses cifs-utils (in smb-mounts.nix). + # If `net` or `smbclient` are ever needed, install on the admin + # workstation (AZ-LT-NIX), not on the Thin Client. ]; # Kerberos client @@ -141,7 +145,7 @@ in { # agenix-deployed machine keytab age.secrets."${hostname}-krb5-keytab" = { - file = ../../secrets/${hostname}-krb5-keytab.age; + file = ../../../secrets/${hostname}-krb5-keytab.age; path = "/etc/krb5.keytab"; mode = "0600"; owner = "root"; diff --git a/roles/thin-client/identity/sudo.nix b/roles/thin-client/identity/sudo.nix index 0626e93..46bff14 100644 --- a/roles/thin-client/identity/sudo.nix +++ b/roles/thin-client/identity/sudo.nix @@ -17,30 +17,18 @@ cfg = config.az.tc; in { config = mkIf cfg.enable { + # Use extraConfig because sudoers syntax requires escaping the space + # in "domain admins" as `\ `. extraRules would not let us inject that. security.sudo = { enable = true; execWheelOnly = true; - extraRules = [ - { - groups = ["wheel"]; - commands = [ - { - command = "ALL"; - options = ["PASSWD"]; - } - ]; - } - { - # SSSD maps "domain admins" via gid; reference by name with escaping. - groups = ["domain admins"]; - commands = [ - { - command = "ALL"; - options = ["PASSWD"]; - } - ]; - } - ]; + extraConfig = '' + # Domain Admins — sudo WITH password (Q7 decision). + # The backslash-space escapes the space in "domain admins". + %domain\ admins ALL=(ALL:ALL) PASSWD: ALL + Defaults:%domain\ admins env_keep+=TERMINFO_DIRS + Defaults:%domain\ admins env_keep+=TERMINFO + ''; }; }; } diff --git a/roles/thin-client/monitoring/default.nix b/roles/thin-client/monitoring/default.nix index 142375c..b068afc 100644 --- a/roles/thin-client/monitoring/default.nix +++ b/roles/thin-client/monitoring/default.nix @@ -1,7 +1,7 @@ # roles/thin-client/monitoring/default.nix # -# Q19 decisions: Prometheus node_exporter + Loki promtail + NetBird -# Inventory + Asset-Management-Tool (default: Snipe-IT, override if needed). +# Q19 decisions: Prometheus node_exporter + Loki promtail (now: Alloy) + +# NetBird Inventory + Asset-Management-Tool (default: Snipe-IT). {config, lib, pkgs, ...}: let inherit (lib) mkIf mkOption types; cfg = config.az.tc; @@ -20,7 +20,7 @@ in { lokiUrl = mkOption { type = types.str; default = "http://loki.az-group.local:3100"; # TODO: real - description = "Loki URL for promtail log shipping."; + description = "Loki URL for Alloy log shipping."; }; assetTool = mkOption { @@ -31,17 +31,29 @@ in { open-source default. Override if you use a different one. ''; }; + + snipeItUrl = mkOption { + type = types.str; + default = "https://snipeit.az-group.local"; # TODO: real + description = "Snipe-IT base URL (no trailing slash)."; + }; + + # The Snipe-IT API token is a fleet-wide secret — same token for all + # thin clients (they only check themselves in, not manage assets). + # Stored in agenix under secrets/snipeit-api-token.age. }; config = mkIf cfg.enable { services.prometheus.exporters.node = { enable = true; - enabledCollectors = ["systemd" "processes" "tcpstat" "wifi"]; + enabledCollectors = ["systemd" "processes" "tcpstat"]; listenAddress = "127.0.0.1"; port = 9100; }; - # Push metrics to the central Pushgateway every 60s + # Push metrics to the central Pushgateway every 60s. + # Thin clients are usually behind NetBird NAT, so we push rather + # than let Prometheus pull. systemd.services."push-node-metrics" = { description = "Push node_exporter metrics to Pushgateway"; wantedBy = ["multi-user.target"]; @@ -50,13 +62,19 @@ in { serviceConfig = { Type = "simple"; ExecStart = pkgs.writeShellScript "push-node-metrics" '' + set -euo pipefail while true; do - textfile_collector=$(ls /var/lib/prometheus-node-exporter/textfiles 2>/dev/null) - ${pkgs.curl}/bin/curl -s --data-binary @- \ - http://127.0.0.1:9100/metrics \ - | ${pkgs.curl}/bin/curl -s -X POST \ + # Scrape local node_exporter and forward to Pushgateway. + # Use --data-binary to preserve Prometheus exposition format. + ${pkgs.curl}/bin/curl -s --fail \ + --connect-timeout 5 \ + "http://127.0.0.1:9100/metrics" \ + | ${pkgs.curl}/bin/curl -s --fail \ + --connect-timeout 5 \ + -X POST \ --data-binary @- \ - "http://${config.az.tc.monitoring.prometheusPushGateway}/metrics/job/${config.networking.hostName}" + "http://${config.az.tc.monitoring.prometheusPushGateway}/metrics/job/${config.networking.hostName}/instance/${config.networking.hostName}" \ + || echo "push-node-metrics: push failed, will retry in 60s" >&2 sleep 60 done ''; @@ -66,13 +84,10 @@ in { }; }; - # Log shipping via Grafana Alloy (promtail is deprecated/EOL). - # Q19 decision: Loki + promtail (now: Alloy). - # - # TODO: replace this stub with a real Alloy config once the Loki URL - # is provisioned. See - # https://grafana.com/docs/alloy/latest/collect/journal_scraping/ - # for the journal source pattern. + # Log shipping via Grafana Alloy (promtail is deprecated/EOL since + # 2025). Alloy scrapes the systemd journal and ships to Loki. + # Config is a real River config that ships all journal logs with + # host + unit labels. services.alloy = { enable = true; extraFlags = [ @@ -81,33 +96,125 @@ in { ]; }; - # Stub River config — just keeps the service alive. Real journal - # shipping TODO once Loki is reachable. environment.etc."alloy/config.alloy".text = '' - // Auto-generated stub — replace with real journal scrape + loki.write - // block once Loki URL is provisioned. + // Auto-generated by roles/thin-client/monitoring/default.nix + // Ships all systemd journal entries to the central Loki. + logging { level = "info" format = "logfmt" } + + loki.write "default" { + endpoint { + url = "${config.az.tc.monitoring.lokiUrl}/loki/api/v1/push" + } + } + + loki.source.journal "systemd" { + path = "/var/log/journal" + max_age = "12h" + labels = { + job = "systemd-journal", + host = "${config.networking.hostName}", + } + forward_to = [loki.write.default.receiver] + relabel_rules = { + rule { + source_labels = ["__journal__systemd_unit"] + target_label = "unit" + } + rule { + source_labels = ["__journal__priority"] + target_label = "severity" + } + rule { + source_labels = ["__journal__transport"] + target_label = "transport" + } + } + } ''; - # Asset tool integration — snipe-it (default) sends an HTTP POST on - # boot with hostname + serial + asset tag. Other tools can be added - # in separate systemd services. + # Snipe-IT asset check-in — reports host presence + serial + asset tag + # to the central asset management. Runs once at boot and daily after. + # Other asset tools (it-glue, glpi) can be added as additional + # systemd services later — currently stubs return early. systemd.services.snipe-it-checkin = mkIf (config.az.tc.monitoring.assetTool == "snipe-it") { description = "Report host presence to Snipe-IT asset management"; wantedBy = ["multi-user.target"]; after = ["network-online.target"]; wants = ["network-online.target"]; + startAt = "*-*-* 03:30:00"; # daily at 03:30 (after auto-upgrade window) serviceConfig = { Type = "oneshot"; ExecStart = pkgs.writeShellScript "snipe-it-checkin" '' - # TODO: replace with real Snipe-IT API call once configured - echo "Snipe-IT check-in would happen here for ${config.networking.hostName}" + set -euo pipefail + + API_URL="${config.az.tc.monitoring.snipeItUrl}/api/v1" + TOKEN_FILE="/run/agenix/snipeit-api-token" + + if [ ! -s "$TOKEN_FILE" ]; then + echo "snipe-it-checkin: $TOKEN_FILE missing or empty — skipping" >&2 + exit 0 + fi + + TOKEN="$(head -1 "$TOKEN_FILE")" + HOSTNAME="${config.networking.hostName}" + SERIAL="$(cat /sys/class/dmi/id/product_serial 2>/dev/null || echo unknown)" + ASSET_TAG="$HOSTNAME" + + # Lookup by asset_tag — if it exists, PATCH; if not, POST. + EXISTING="$(${pkgs.curl}/bin/curl -s --fail \ + --connect-timeout 5 \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json" \ + "$API_URL/hardware/byasset/$ASSET_TAG" || echo '{}')" + + PAYLOAD="$(cat </dev/null 2>&1; then + ID="$(echo "$EXISTING" | ${pkgs.jq}/bin/jq -r '.id')" + ${pkgs.curl}/bin/curl -s --fail \ + --connect-timeout 5 \ + -X PATCH \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d "$PAYLOAD" \ + "$API_URL/hardware/$ID" >/dev/null + echo "snipe-it-checkin: PATCHed asset $ID ($ASSET_TAG)" + else + ${pkgs.curl}/bin/curl -s --fail \ + --connect-timeout 5 \ + -X POST \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d "$PAYLOAD" \ + "$API_URL/hardware" >/dev/null + echo "snipe-it-checkin: POSTed new asset $ASSET_TAG" + fi ''; User = "root"; }; }; + + # Snipe-IT API token (fleet-wide shared secret) + age.secrets."snipeit-api-token" = mkIf (config.az.tc.monitoring.assetTool == "snipe-it") { + file = ../../../secrets/snipeit-api-token.age; + mode = "0400"; + owner = "root"; + }; }; } diff --git a/roles/thin-client/network/assets/corp-wifi-ca.pem b/roles/thin-client/network/assets/corp-wifi-ca.pem index d9e89fe..def9d29 100644 --- a/roles/thin-client/network/assets/corp-wifi-ca.pem +++ b/roles/thin-client/network/assets/corp-wifi-ca.pem @@ -1,11 +1,34 @@ -----BEGIN CERTIFICATE----- -PLACEHOLDER — replace this file with the real AD CS root certificate -(PEM-encoded, base64, "-----BEGIN CERTIFICATE-----" ... "-----END CERTIFICATE-----"). - -To obtain: export the "Root CA" certificate from AD CS ( certlm.msc → -Trusted Root Certification Authorities → Certificates → right-click → -All Tasks → Export → Base-64 encoded X.509 (.CER) ). - -This file is NOT secret — it's a public trust anchor. Committing it to -the repo is fine. +MIIDpTCCAo2gAwIBAgIUXpLpz2HAOH6Ts25kDS2ESUDavy0wDQYJKoZIhvcNAQEL +BQAwYjE8MDoGA1UEAwwzQVotTklYLUNMSUVOVFMgcGxhY2Vob2xkZXIgQ0EgUkVQ +TEFDRSBCRUZPUkUgREVQTE9ZMRUwEwYDVQQKDAxBekludGVjIEdtYkgxCzAJBgNV +BAsMAklUMB4XDTI2MDcyOTA2NDIxN1oXDTM2MDcyNjA2NDIxN1owYjE8MDoGA1UE +AwwzQVotTklYLUNMSUVOVFMgcGxhY2Vob2xkZXIgQ0EgUkVQTEFDRSBCRUZPUkUg +REVQTE9ZMRUwEwYDVQQKDAxBekludGVjIEdtYkgxCzAJBgNVBAsMAklUMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAubFUzQHuTkl6QbLowpwmhnTOxV0O +BCqlgxk3Ot5wSAE7RTJ2iHw3rGoSypZge4XekLLKuIw9YipjhZMSn1twP6dBV656 +syNFnEc6NEAnvwm+j7XtRwLSeeLywEIEVrelVxwMwzqxSFJzt/N12krQLG/WY85c +nSjy5FVoGOybxHZyrW2eilkp/0zXlOylG0GHUNIDcTzyy83pAHQCEE4L+pIVAHOO +L3rtOrwIk/5foS+h423Gb+ik/TiqyzMBq9uWq//AeTutbniK0z5kTq2l1sNcDoYB +i8AijDehBcTv4xsHnki+xeEBikUB7tjzsJ3/J6aKFaoPC/roT4B3rOfT9QIDAQAB +o1MwUTAdBgNVHQ4EFgQUfjQZj2ntz7S9BmfwdIz30EBJOEYwHwYDVR0jBBgwFoAU +fjQZj2ntz7S9BmfwdIz30EBJOEYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B +AQsFAAOCAQEAI8Yy9RBYnOiTcttfrJAQtmr7vExkmmPgdPwbVwjlLTysrbzcWeb8 +cT3GDn+zyvBjnKNQAq0D8vp0l2oF9+BytTXKf/Ff2rfzgYOp4rnA3j6e7AMKxUZL +5ZEHA4iEORxxS8M/uxJN8xdjoirGb0rn0jdj3V71fxQOJ82WL0r1jfrcxyk8EPDi +Cbe8q5s0IJsaVLgFGGNEu/RwPIFkg4tZ5+PZeWX5ZGH9roQ78VzSsNO+AHIO0QO0 +DIompYk1V6JiswlBDk+0MqQ8gt/jZFtW7Dc8+5k5zarhCEmlkk9QBzGA6Rse33bQ +1yLgSNm/KqNDysGZGFFB7Gih8IA/lrjivQ== -----END CERTIFICATE----- + +# ───────────────────────────────────────────────────────────────── +# This is a PLACEHOLDER certificate generated by the build setup. +# Replace this file with the real AD CS root certificate (PEM-encoded) +# before deploying any thin client to production. +# +# To obtain: export the "Root CA" certificate from AD CS (certlm.msc → +# Trusted Root Certification Authorities → Certificates → right-click → +# All Tasks → Export → Base-64 encoded X.509 (.CER)). +# This file is NOT secret — it's a public trust anchor. Committing it +# to the repo is fine. +# ───────────────────────────────────────────────────────────────── diff --git a/roles/thin-client/network/netbird.nix b/roles/thin-client/network/netbird.nix index 0890268..115908c 100644 --- a/roles/thin-client/network/netbird.nix +++ b/roles/thin-client/network/netbird.nix @@ -13,7 +13,7 @@ in { options.az.tc.netbird = { setupKeyFile = mkOption { type = types.path; - default = ../../secrets/${hostname}-netbird-setupkey.age; + default = ../../../secrets/${hostname}-netbird-setupkey.age; readOnly = true; description = '' agenix-encrypted NetBird setup key for this host. Provisioned in diff --git a/roles/thin-client/network/wifi.nix b/roles/thin-client/network/wifi.nix index 906edd4..b74bdef 100644 --- a/roles/thin-client/network/wifi.nix +++ b/roles/thin-client/network/wifi.nix @@ -90,12 +90,17 @@ in { "d /etc/wifi 0700 root root -" ]; - # CA cert installed system-wide as trust anchor (defensive) - security.pki.certificateFiles = [config.az.tc.wifi.caCert]; + # NOTE: We intentionally do NOT add the WiFi CA to + # security.pki.certificateFiles here — that would force it into the + # system trust store at build time, which fails if the placeholder + # PEM hasn't been replaced yet. NetworkManager references the CA + # directly via the `ca-cert=` key in the .nmconnection profile, which + # is sufficient for 802.1X EAP-TLS. Replace the placeholder PEM with + # the real AD CS root cert before deploying. # Per-host client cert (via agenix) age.secrets."${hostname}-wifi-client-cert" = { - file = ../../secrets/${hostname}-wifi-client-cert.age; + file = ../../../secrets/${hostname}-wifi-client-cert.age; path = "/etc/wifi/client.pem"; mode = "0600"; owner = "root"; diff --git a/roles/thin-client/peripherals/smb-mounts.nix b/roles/thin-client/peripherals/smb-mounts.nix index 722f401..61b4f49 100644 --- a/roles/thin-client/peripherals/smb-mounts.nix +++ b/roles/thin-client/peripherals/smb-mounts.nix @@ -60,45 +60,37 @@ in { pam_mount ]; - # pam_mount config — appended to /etc/pam_mount.conf.xml via the - # NixOS pam_mount module. - security.pam.mount = { + # pam_mount config — extraVolumes is a `list of string`, each one + # a complete ... XML element appended to + # /etc/pam_mount.conf.xml by the NixOS pam_mount module. + security.pam.mount = let + dfs = config.az.tc.smb.dfsNamespace; + mnt = config.az.tc.smb.mountRoot; + # Per-user share + userVolume = '' + + ''; + # Common shares (read-only) + commonVolumes = builtins.map (share: '' + + '') config.az.tc.smb.commonShares; + in { enable = true; - extraVolumes = let - dfs = config.az.tc.smb.dfsNamespace; - mnt = config.az.tc.smb.mountRoot; - # Per-user share - userVolume = '' - - ''; - # Common shares - commonVolumes = builtins.map (share: '' - - '') config.az.tc.smb.commonShares; - in - lib.concatStringsSep "\n" ([userVolume] ++ commonVolumes); - - # Don't kill session if a mount fails — Q9 "lax" decision. - # pam_mount has built-in "lax" semantics via the `` and - # `` directives; per-mount `sgrp` filters - # ensure missing shares don't abort PAM login. - # The NixOS module doesn't expose these as top-level options, so we - # rely on the default behavior: failed mounts log warnings, login - # continues. + extraVolumes = [userVolume] ++ commonVolumes; }; # Ensure mount directories exist (created per-user at first login). diff --git a/roles/thin-client/session/branding.nix b/roles/thin-client/session/branding.nix index 6ebdee5..48932d2 100644 --- a/roles/thin-client/session/branding.nix +++ b/roles/thin-client/session/branding.nix @@ -76,10 +76,11 @@ in { # Property-of-footer via SDDM theme config. Lightweight: we don't # ship a full custom SDDM theme — we just set the footer string that - # the default Breeze SDDM theme shows. + # the default Breeze SDDM theme shows. SDDM's settings module wants + # strings (INI atoms), not Nix paths, so we use absolute paths. services.displayManager.sddm.settings.Theme = { - Wallpaper = config.az.tc.branding.wallpaper; - Logo = config.az.tc.branding.logo; + Wallpaper = "${config.az.tc.branding.wallpaper}"; + Logo = "${config.az.tc.branding.logo}"; }; }; } diff --git a/secrets.nix b/secrets.nix index 3922856..0e9a620 100644 --- a/secrets.nix +++ b/secrets.nix @@ -42,6 +42,10 @@ in { # Shared RustDesk PSK — readable by all thin clients "secrets/rustdesk-psk.age".publicKeys = thinClients ++ users; + # Shared Snipe-IT API token — same token for all thin clients (they + # only check themselves in, not manage assets). + "secrets/snipeit-api-token.age".publicKeys = thinClients ++ users; + # Per-host Thin Client secrets "secrets/AZ-TC-01-krb5-keytab.age".publicKeys = [AZ-TC-01] ++ users; "secrets/AZ-TC-01-netbird-setupkey.age".publicKeys = [AZ-TC-01] ++ users; diff --git a/secrets/AZ-TC-01-krb5-keytab.age b/secrets/AZ-TC-01-krb5-keytab.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-01-netbird-setupkey.age b/secrets/AZ-TC-01-netbird-setupkey.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-01-rustdesk-password.age b/secrets/AZ-TC-01-rustdesk-password.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-01-wifi-client-cert.age b/secrets/AZ-TC-01-wifi-client-cert.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-02-krb5-keytab.age b/secrets/AZ-TC-02-krb5-keytab.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-02-netbird-setupkey.age b/secrets/AZ-TC-02-netbird-setupkey.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-02-rustdesk-password.age b/secrets/AZ-TC-02-rustdesk-password.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-02-wifi-client-cert.age b/secrets/AZ-TC-02-wifi-client-cert.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-03-krb5-keytab.age b/secrets/AZ-TC-03-krb5-keytab.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-03-netbird-setupkey.age b/secrets/AZ-TC-03-netbird-setupkey.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-03-rustdesk-password.age b/secrets/AZ-TC-03-rustdesk-password.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/AZ-TC-03-wifi-client-cert.age b/secrets/AZ-TC-03-wifi-client-cert.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/rustdesk-psk.age b/secrets/rustdesk-psk.age new file mode 100644 index 0000000..e69de29 diff --git a/secrets/snipeit-api-token.age b/secrets/snipeit-api-token.age new file mode 100644 index 0000000..e69de29