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)
This commit is contained in:
m3ta-chiron
2026-07-29 09:06:12 +02:00
parent 98dc2917e8
commit 0cc0b5064d
31 changed files with 282 additions and 116 deletions
+20 -3
View File
@@ -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.
+1 -1
View File
@@ -78,7 +78,7 @@ in {
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
remmina
freerdp3
freerdp
];
# System-wide Remmina profiles
+2 -2
View File
@@ -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";
};
+20
View File
@@ -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.";
}
];
};
}
+2 -2
View File
@@ -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
'';
+11 -7
View File
@@ -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";
+9 -21
View File
@@ -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
'';
};
};
}
+133 -26
View File
@@ -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 <<JSON
{
"asset_tag": "$ASSET_TAG",
"name": "$HOSTNAME",
"serial": "$SERIAL",
"model_id": 1,
"status_id": 2,
"notes": "AZ-NIX-CLIENTS thin client (auto-checked-in)"
}
JSON
)"
if echo "$EXISTING" | ${pkgs.jq}/bin/jq -e '.id' >/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";
};
};
}
@@ -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.
# ─────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -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
+8 -3
View File
@@ -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";
+30 -38
View File
@@ -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 <volume>...</volume> 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 = ''
<volume
fstype="cifs"
server="${dfs}"
path="${config.az.tc.smb.userShare}"
mountpoint="${mnt}/home"
options="sec=krb5i,cruid=%(USER),uid=%(USER),gid=%(USER),multiuser,nodev,nosuid,mfsymlinks"
user="*"
/>
'';
# Common shares (read-only)
commonVolumes = builtins.map (share: ''
<volume
fstype="cifs"
server="${dfs}"
path="${share}"
mountpoint="${mnt}/${share}"
options="sec=krb5i,cruid=%(USER),uid=%(USER),gid=%(USER),multiuser,nodev,nosuid,ro,mfsymlinks"
user="*"
/>
'') 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 = ''
<volume
fstype="cifs"
server="${dfs}"
path="${config.az.tc.smb.userShare}"
mountpoint="${mnt}/home"
options="sec=krb5i,cruid=%(USER),uid=%(USER),gid=%(USER),multiuser,nodev,nosuid,mfsymlinks"
user="*"
/>
'';
# Common shares
commonVolumes = builtins.map (share: ''
<volume
fstype="cifs"
server="${dfs}"
path="${share}"
mountpoint="${mnt}/${share}"
options="sec=krb5i,cruid=%(USER),uid=%(USER),gid=%(USER),multiuser,nodev,nosuid,ro,mfsymlinks"
user="*"
/>
'') 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 `<luserconf>` and
# `<mkmountpoint enable="...">` 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).
+4 -3
View File
@@ -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}";
};
};
}