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:
@@ -11,6 +11,9 @@
|
|||||||
az.tc = {
|
az.tc = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hardwareClass = "dell-optiplex-micro";
|
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";
|
networking.hostName = "AZ-TC-01";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
az.tc = {
|
az.tc = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hardwareClass = "dell-optiplex-micro";
|
hardwareClass = "dell-optiplex-micro";
|
||||||
|
site = "staging";
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.hostName = "AZ-TC-02";
|
networking.hostName = "AZ-TC-02";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
az.tc = {
|
az.tc = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hardwareClass = "dell-optiplex-micro";
|
hardwareClass = "dell-optiplex-micro";
|
||||||
|
site = "staging";
|
||||||
};
|
};
|
||||||
|
|
||||||
networking.hostName = "AZ-TC-03";
|
networking.hostName = "AZ-TC-03";
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Quick reference:
|
|||||||
| Printers | Single Pull-Print queue, direct IPPS, Avahi off |
|
| Printers | Single Pull-Print queue, direct IPPS, Avahi off |
|
||||||
| Branding | Light: corporate wallpaper + SDDM logo + property footer, no banner |
|
| Branding | Light: corporate wallpaper + SDDM logo + property footer, no banner |
|
||||||
| Updates | `system.autoUpgrade` daily at 03:00, reboot window 03:00-05:00 |
|
| 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 |
|
| Rollout | Pilot: 3 hosts, 2 weeks; then 5 → 10 → rest |
|
||||||
|
|
||||||
## Provisioning workflow for a new Fleet Host
|
## 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.branding.company` | `AzIntec GmbH` | Company name |
|
||||||
| `az.tc.monitoring.prometheusPushGateway` | `pushgateway.az-group.local:9091` | Pushgateway URL |
|
| `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.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.monitoring.assetTool` | `snipe-it` | Asset tool integration |
|
||||||
| `az.tc.deployment.diskDevice` | `/dev/sda` | Disko target disk |
|
| `az.tc.deployment.diskDevice` | `/dev/sda` | Disko target disk |
|
||||||
| `az.tc.deployment.swapSizeGB` | `4` | Swap size in GB |
|
| `az.tc.deployment.swapSizeGB` | `4` | Swap size in GB |
|
||||||
@@ -206,9 +207,25 @@ To validate the config without deploying:
|
|||||||
# Eval-check all hosts
|
# Eval-check all hosts
|
||||||
nix flake check
|
nix flake check
|
||||||
|
|
||||||
# Build a pilot host closure
|
# Build a pilot host closure (full closure, takes ~20 min uncached)
|
||||||
nix build .#nixosConfigurations.AZ-TC-01.config.system.build.toplevel
|
nix build --no-link .#nixosConfigurations.AZ-TC-01.config.system.build.toplevel
|
||||||
|
|
||||||
# Try a VM boot
|
# Try a VM boot
|
||||||
nix run .#nixosConfigurations.AZ-TC-01.config.system.build.vm
|
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.
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ in {
|
|||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
remmina
|
remmina
|
||||||
freerdp3
|
freerdp
|
||||||
];
|
];
|
||||||
|
|
||||||
# System-wide Remmina profiles
|
# System-wide Remmina profiles
|
||||||
|
|||||||
@@ -67,14 +67,14 @@ in {
|
|||||||
|
|
||||||
# Shared pre-shared key (one for all thin clients)
|
# Shared pre-shared key (one for all thin clients)
|
||||||
age.secrets."rustdesk-psk" = {
|
age.secrets."rustdesk-psk" = {
|
||||||
file = ../../secrets/rustdesk-psk.age;
|
file = ../../../secrets/rustdesk-psk.age;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = "root";
|
owner = "root";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Per-host permanent password
|
# Per-host permanent password
|
||||||
age.secrets."${hostname}-rustdesk-password" = {
|
age.secrets."${hostname}-rustdesk-password" = {
|
||||||
file = ../../secrets/${hostname}-rustdesk-password.age;
|
file = ../../../secrets/${hostname}-rustdesk-password.age;
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = "root";
|
owner = "root";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,11 +104,31 @@ in {
|
|||||||
# Firewall is enabled by the network module.
|
# Firewall is enabled by the network module.
|
||||||
|
|
||||||
# ── assertions / warnings ────────────────────────────────────────
|
# ── 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 = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = cfg.enable -> (config.networking.hostName or "") != "";
|
assertion = cfg.enable -> (config.networking.hostName or "") != "";
|
||||||
message = "az.tc.enable requires networking.hostName to be set (e.g. 'AZ-TC-01').";
|
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.";
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# before and after each `nixos-rebuild switch`, plus hourly/daily.
|
# before and after each `nixos-rebuild switch`, plus hourly/daily.
|
||||||
#
|
#
|
||||||
# Q18 decision: Generation-Picker + BTRFS-Snapshot.
|
# Q18 decision: Generation-Picker + BTRFS-Snapshot.
|
||||||
{config, lib, ...}: let
|
{config, lib, pkgs, ...}: let
|
||||||
inherit (lib) mkIf;
|
inherit (lib) mkIf;
|
||||||
cfg = config.az.tc;
|
cfg = config.az.tc;
|
||||||
in {
|
in {
|
||||||
@@ -31,7 +31,7 @@ in {
|
|||||||
deps = [];
|
deps = [];
|
||||||
text = ''
|
text = ''
|
||||||
if [ -d /.snapshots ]; then
|
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
|
-d "pre-switch $(date +%Y-%m-%d-%H%M)" || true
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -53,12 +53,16 @@ in {
|
|||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
adcli
|
adcli # AD computer-account precreate + join helper
|
||||||
sssd
|
sssd # AD client daemon
|
||||||
samba4Full
|
krb5 # MIT Kerberos client
|
||||||
krb5
|
realmd # DBus service for realm discovery (used by adcli wrapper)
|
||||||
realmd
|
# NOTE: samba4Full intentionally NOT included — it would pull in
|
||||||
oddjob
|
# 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
|
# Kerberos client
|
||||||
@@ -141,7 +145,7 @@ in {
|
|||||||
|
|
||||||
# agenix-deployed machine keytab
|
# agenix-deployed machine keytab
|
||||||
age.secrets."${hostname}-krb5-keytab" = {
|
age.secrets."${hostname}-krb5-keytab" = {
|
||||||
file = ../../secrets/${hostname}-krb5-keytab.age;
|
file = ../../../secrets/${hostname}-krb5-keytab.age;
|
||||||
path = "/etc/krb5.keytab";
|
path = "/etc/krb5.keytab";
|
||||||
mode = "0600";
|
mode = "0600";
|
||||||
owner = "root";
|
owner = "root";
|
||||||
|
|||||||
@@ -17,30 +17,18 @@
|
|||||||
cfg = config.az.tc;
|
cfg = config.az.tc;
|
||||||
in {
|
in {
|
||||||
config = mkIf cfg.enable {
|
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 = {
|
security.sudo = {
|
||||||
enable = true;
|
enable = true;
|
||||||
execWheelOnly = true;
|
execWheelOnly = true;
|
||||||
extraRules = [
|
extraConfig = ''
|
||||||
{
|
# Domain Admins — sudo WITH password (Q7 decision).
|
||||||
groups = ["wheel"];
|
# The backslash-space escapes the space in "domain admins".
|
||||||
commands = [
|
%domain\ admins ALL=(ALL:ALL) PASSWD: ALL
|
||||||
{
|
Defaults:%domain\ admins env_keep+=TERMINFO_DIRS
|
||||||
command = "ALL";
|
Defaults:%domain\ admins env_keep+=TERMINFO
|
||||||
options = ["PASSWD"];
|
'';
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
# SSSD maps "domain admins" via gid; reference by name with escaping.
|
|
||||||
groups = ["domain admins"];
|
|
||||||
commands = [
|
|
||||||
{
|
|
||||||
command = "ALL";
|
|
||||||
options = ["PASSWD"];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# roles/thin-client/monitoring/default.nix
|
# roles/thin-client/monitoring/default.nix
|
||||||
#
|
#
|
||||||
# Q19 decisions: Prometheus node_exporter + Loki promtail + NetBird
|
# Q19 decisions: Prometheus node_exporter + Loki promtail (now: Alloy) +
|
||||||
# Inventory + Asset-Management-Tool (default: Snipe-IT, override if needed).
|
# NetBird Inventory + Asset-Management-Tool (default: Snipe-IT).
|
||||||
{config, lib, pkgs, ...}: let
|
{config, lib, pkgs, ...}: let
|
||||||
inherit (lib) mkIf mkOption types;
|
inherit (lib) mkIf mkOption types;
|
||||||
cfg = config.az.tc;
|
cfg = config.az.tc;
|
||||||
@@ -20,7 +20,7 @@ in {
|
|||||||
lokiUrl = mkOption {
|
lokiUrl = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "http://loki.az-group.local:3100"; # TODO: real
|
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 {
|
assetTool = mkOption {
|
||||||
@@ -31,17 +31,29 @@ in {
|
|||||||
open-source default. Override if you use a different one.
|
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 {
|
config = mkIf cfg.enable {
|
||||||
services.prometheus.exporters.node = {
|
services.prometheus.exporters.node = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enabledCollectors = ["systemd" "processes" "tcpstat" "wifi"];
|
enabledCollectors = ["systemd" "processes" "tcpstat"];
|
||||||
listenAddress = "127.0.0.1";
|
listenAddress = "127.0.0.1";
|
||||||
port = 9100;
|
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" = {
|
systemd.services."push-node-metrics" = {
|
||||||
description = "Push node_exporter metrics to Pushgateway";
|
description = "Push node_exporter metrics to Pushgateway";
|
||||||
wantedBy = ["multi-user.target"];
|
wantedBy = ["multi-user.target"];
|
||||||
@@ -50,13 +62,19 @@ in {
|
|||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
ExecStart = pkgs.writeShellScript "push-node-metrics" ''
|
ExecStart = pkgs.writeShellScript "push-node-metrics" ''
|
||||||
|
set -euo pipefail
|
||||||
while true; do
|
while true; do
|
||||||
textfile_collector=$(ls /var/lib/prometheus-node-exporter/textfiles 2>/dev/null)
|
# Scrape local node_exporter and forward to Pushgateway.
|
||||||
${pkgs.curl}/bin/curl -s --data-binary @- \
|
# Use --data-binary to preserve Prometheus exposition format.
|
||||||
http://127.0.0.1:9100/metrics \
|
${pkgs.curl}/bin/curl -s --fail \
|
||||||
| ${pkgs.curl}/bin/curl -s -X POST \
|
--connect-timeout 5 \
|
||||||
|
"http://127.0.0.1:9100/metrics" \
|
||||||
|
| ${pkgs.curl}/bin/curl -s --fail \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
-X POST \
|
||||||
--data-binary @- \
|
--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
|
sleep 60
|
||||||
done
|
done
|
||||||
'';
|
'';
|
||||||
@@ -66,13 +84,10 @@ in {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Log shipping via Grafana Alloy (promtail is deprecated/EOL).
|
# Log shipping via Grafana Alloy (promtail is deprecated/EOL since
|
||||||
# Q19 decision: Loki + promtail (now: Alloy).
|
# 2025). Alloy scrapes the systemd journal and ships to Loki.
|
||||||
#
|
# Config is a real River config that ships all journal logs with
|
||||||
# TODO: replace this stub with a real Alloy config once the Loki URL
|
# host + unit labels.
|
||||||
# is provisioned. See
|
|
||||||
# https://grafana.com/docs/alloy/latest/collect/journal_scraping/
|
|
||||||
# for the journal source pattern.
|
|
||||||
services.alloy = {
|
services.alloy = {
|
||||||
enable = true;
|
enable = true;
|
||||||
extraFlags = [
|
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 = ''
|
environment.etc."alloy/config.alloy".text = ''
|
||||||
// Auto-generated stub — replace with real journal scrape + loki.write
|
// Auto-generated by roles/thin-client/monitoring/default.nix
|
||||||
// block once Loki URL is provisioned.
|
// Ships all systemd journal entries to the central Loki.
|
||||||
|
|
||||||
logging {
|
logging {
|
||||||
level = "info"
|
level = "info"
|
||||||
format = "logfmt"
|
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
|
# Snipe-IT asset check-in — reports host presence + serial + asset tag
|
||||||
# boot with hostname + serial + asset tag. Other tools can be added
|
# to the central asset management. Runs once at boot and daily after.
|
||||||
# in separate systemd services.
|
# 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") {
|
systemd.services.snipe-it-checkin = mkIf (config.az.tc.monitoring.assetTool == "snipe-it") {
|
||||||
description = "Report host presence to Snipe-IT asset management";
|
description = "Report host presence to Snipe-IT asset management";
|
||||||
wantedBy = ["multi-user.target"];
|
wantedBy = ["multi-user.target"];
|
||||||
after = ["network-online.target"];
|
after = ["network-online.target"];
|
||||||
wants = ["network-online.target"];
|
wants = ["network-online.target"];
|
||||||
|
startAt = "*-*-* 03:30:00"; # daily at 03:30 (after auto-upgrade window)
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
ExecStart = pkgs.writeShellScript "snipe-it-checkin" ''
|
ExecStart = pkgs.writeShellScript "snipe-it-checkin" ''
|
||||||
# TODO: replace with real Snipe-IT API call once configured
|
set -euo pipefail
|
||||||
echo "Snipe-IT check-in would happen here for ${config.networking.hostName}"
|
|
||||||
|
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";
|
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-----
|
-----BEGIN CERTIFICATE-----
|
||||||
PLACEHOLDER — replace this file with the real AD CS root certificate
|
MIIDpTCCAo2gAwIBAgIUXpLpz2HAOH6Ts25kDS2ESUDavy0wDQYJKoZIhvcNAQEL
|
||||||
(PEM-encoded, base64, "-----BEGIN CERTIFICATE-----" ... "-----END CERTIFICATE-----").
|
BQAwYjE8MDoGA1UEAwwzQVotTklYLUNMSUVOVFMgcGxhY2Vob2xkZXIgQ0EgUkVQ
|
||||||
|
TEFDRSBCRUZPUkUgREVQTE9ZMRUwEwYDVQQKDAxBekludGVjIEdtYkgxCzAJBgNV
|
||||||
To obtain: export the "Root CA" certificate from AD CS ( certlm.msc →
|
BAsMAklUMB4XDTI2MDcyOTA2NDIxN1oXDTM2MDcyNjA2NDIxN1owYjE8MDoGA1UE
|
||||||
Trusted Root Certification Authorities → Certificates → right-click →
|
AwwzQVotTklYLUNMSUVOVFMgcGxhY2Vob2xkZXIgQ0EgUkVQTEFDRSBCRUZPUkUg
|
||||||
All Tasks → Export → Base-64 encoded X.509 (.CER) ).
|
REVQTE9ZMRUwEwYDVQQKDAxBekludGVjIEdtYkgxCzAJBgNVBAsMAklUMIIBIjAN
|
||||||
|
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAubFUzQHuTkl6QbLowpwmhnTOxV0O
|
||||||
This file is NOT secret — it's a public trust anchor. Committing it to
|
BCqlgxk3Ot5wSAE7RTJ2iHw3rGoSypZge4XekLLKuIw9YipjhZMSn1twP6dBV656
|
||||||
the repo is fine.
|
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-----
|
-----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.
|
||||||
|
# ─────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ in {
|
|||||||
options.az.tc.netbird = {
|
options.az.tc.netbird = {
|
||||||
setupKeyFile = mkOption {
|
setupKeyFile = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = ../../secrets/${hostname}-netbird-setupkey.age;
|
default = ../../../secrets/${hostname}-netbird-setupkey.age;
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
description = ''
|
description = ''
|
||||||
agenix-encrypted NetBird setup key for this host. Provisioned in
|
agenix-encrypted NetBird setup key for this host. Provisioned in
|
||||||
|
|||||||
@@ -90,12 +90,17 @@ in {
|
|||||||
"d /etc/wifi 0700 root root -"
|
"d /etc/wifi 0700 root root -"
|
||||||
];
|
];
|
||||||
|
|
||||||
# CA cert installed system-wide as trust anchor (defensive)
|
# NOTE: We intentionally do NOT add the WiFi CA to
|
||||||
security.pki.certificateFiles = [config.az.tc.wifi.caCert];
|
# 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)
|
# Per-host client cert (via agenix)
|
||||||
age.secrets."${hostname}-wifi-client-cert" = {
|
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";
|
path = "/etc/wifi/client.pem";
|
||||||
mode = "0600";
|
mode = "0600";
|
||||||
owner = "root";
|
owner = "root";
|
||||||
|
|||||||
@@ -60,45 +60,37 @@ in {
|
|||||||
pam_mount
|
pam_mount
|
||||||
];
|
];
|
||||||
|
|
||||||
# pam_mount config — appended to /etc/pam_mount.conf.xml via the
|
# pam_mount config — extraVolumes is a `list of string`, each one
|
||||||
# NixOS pam_mount module.
|
# a complete <volume>...</volume> XML element appended to
|
||||||
security.pam.mount = {
|
# /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;
|
enable = true;
|
||||||
extraVolumes = let
|
extraVolumes = [userVolume] ++ commonVolumes;
|
||||||
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.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Ensure mount directories exist (created per-user at first login).
|
# Ensure mount directories exist (created per-user at first login).
|
||||||
|
|||||||
@@ -76,10 +76,11 @@ in {
|
|||||||
|
|
||||||
# Property-of-footer via SDDM theme config. Lightweight: we don't
|
# Property-of-footer via SDDM theme config. Lightweight: we don't
|
||||||
# ship a full custom SDDM theme — we just set the footer string that
|
# 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 = {
|
services.displayManager.sddm.settings.Theme = {
|
||||||
Wallpaper = config.az.tc.branding.wallpaper;
|
Wallpaper = "${config.az.tc.branding.wallpaper}";
|
||||||
Logo = config.az.tc.branding.logo;
|
Logo = "${config.az.tc.branding.logo}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ in {
|
|||||||
# Shared RustDesk PSK — readable by all thin clients
|
# Shared RustDesk PSK — readable by all thin clients
|
||||||
"secrets/rustdesk-psk.age".publicKeys = thinClients ++ users;
|
"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
|
# Per-host Thin Client secrets
|
||||||
"secrets/AZ-TC-01-krb5-keytab.age".publicKeys = [AZ-TC-01] ++ users;
|
"secrets/AZ-TC-01-krb5-keytab.age".publicKeys = [AZ-TC-01] ++ users;
|
||||||
"secrets/AZ-TC-01-netbird-setupkey.age".publicKeys = [AZ-TC-01] ++ users;
|
"secrets/AZ-TC-01-netbird-setupkey.age".publicKeys = [AZ-TC-01] ++ users;
|
||||||
|
|||||||
Reference in New Issue
Block a user