feat: +thin-client role + AZ-TC-01..03 pilot
Auf Basis des Grilling-Sessions mit 20 design decisions umgesetztes v1-Skeleton der Thin-Client-Rolle plus 3 Pilot-Hosts. Architektur: - roles/thin-client/ als geschlossene Rolle (default.nix compose + 7 Subdirectories: hardware, session, identity, network, peripherals, apps, monitoring, deployment) - hosts/AZ-TC-NN/default.nix als ~20-Zeilen-Wrapper pro Fleet-Host - flake.nix instanziiert AZ-TC-01..03 via Fleet-Helper - secrets.nix mit per-host agenix-Secret-Stubs Submodule: - hardware: dell-optiplex-micro + generic-x86_64-uefi Fallback - identity: AD (sssd/krb5/keytab via agenix), lokale Notfalluser (sascha.koenig + jannik.mueller ohne m3ta-home), sudo-Policy - session: KDE Plasma 6 + Wayland + SDDM, Branding (Wallpaper + Footer), PipeWire Audio - network: NetworkManager + wpa_supplicant + 802.1X EAP-TLS, NetBird + SSH via NetBird, systemd-resolved (Corp + NetBird DNS), hardened firewall, OpenSSH - peripherals: CUPS mit Pull-Print-Queue, pam_mount für DFS-Shares - apps: Chromium (ManagedBookmarks, Bitwarden force-install, no local passwords), Office-Web .desktop-Shortcuts, Remmina (mehrere TS, Kerberos SSO), RustDesk Client + Daemon, OBS Studio, Autostart - monitoring: node_exporter → Pushgateway, Alloy (stub für Loki), Snipe-IT Asset-Checkin (stub) - deployment: Disko BTRFS-Layout, auto-upgrade daily + reboot window, snapper snapshots Build-Validierung: 'nix flake check' bestanden für AZ-TC-01/02/03. Siehe roles/thin-client/README.md für den Provisionierungs-Workflow und die Liste der noch auszufüllenden Platzhalter (TODO-Kommentare in den jeweiligen Modulen).
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
# roles/thin-client/identity/ad.nix
|
||||
#
|
||||
# Active Directory integration for Thin Clients.
|
||||
#
|
||||
# Real realm: AZ-GROUP
|
||||
# DNS domain: az-group.local
|
||||
# Join mechanism: Pre-created computer accounts + keytab via agenix
|
||||
# (no interactive realm join, no service account with join privileges).
|
||||
#
|
||||
# Provisioning per host (admin-side, one-shot):
|
||||
# 1. 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" \
|
||||
# <admin>@AZ-GROUP
|
||||
# 2. adcli join --computer-name=AZ-TC-01$ \
|
||||
# --domain=az-group.local \
|
||||
# --host-fqdn=AZ-TC-01.az-group.local \
|
||||
# --login-type=computer \
|
||||
# --user=<admin> --verbose \
|
||||
# -K /tmp/AZ-TC-01.keytab
|
||||
# 3. agenix -e secrets/AZ-TC-01-krb5-keytab.age (paste /tmp/AZ-TC-01.keytab)
|
||||
# 4. rm /tmp/AZ-TC-01.keytab
|
||||
# 5. Deploy; the keytab decrypts to /etc/krb5.keytab on the host.
|
||||
#
|
||||
# SSSD resolves users/groups via AD; kerberos auth via keytab for the
|
||||
# machine account. User login uses their AD password (offline-capable via
|
||||
# cache_credentials=true).
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
cfg = config.az.tc;
|
||||
hostname = config.networking.hostName;
|
||||
domain = "az-group.local";
|
||||
realm = "AZ-GROUP";
|
||||
in {
|
||||
options.az.tc.ad = {
|
||||
ou = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "OU=ThinClients,DC=az-group,DC=local";
|
||||
description = ''
|
||||
AD Organizational Unit where Thin Client computer objects live.
|
||||
Override if your AD layout differs. Used for documentation and
|
||||
the pre-create workflow; not consumed at runtime.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages = with pkgs; [
|
||||
adcli
|
||||
sssd
|
||||
samba4Full
|
||||
krb5
|
||||
realmd
|
||||
oddjob
|
||||
];
|
||||
|
||||
# Kerberos client
|
||||
security.krb5 = {
|
||||
enable = true;
|
||||
settings = {
|
||||
libdefaults = {
|
||||
default_realm = realm;
|
||||
udp_preference_limit = 0;
|
||||
forwardable = true;
|
||||
proxiable = true;
|
||||
rdns = false;
|
||||
};
|
||||
domain_realm = {
|
||||
".az-group.local" = realm;
|
||||
"az-group.local" = realm;
|
||||
};
|
||||
realms = {
|
||||
"${realm}" = {
|
||||
kdc = ["az-dc01.az-group.local" "az-dc02.az-group.local"];
|
||||
admin_server = "az-dc01.az-group.local";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# PAM: create per-user /home on first login (used by domain users)
|
||||
security.pam = {
|
||||
services.login.makeHomeDir = true;
|
||||
services.sddm.makeHomeDir = true;
|
||||
services.sshd.makeHomeDir = true;
|
||||
makeHomeDir.umask = "077";
|
||||
};
|
||||
|
||||
services.nscd.enable = true;
|
||||
|
||||
services.sssd = {
|
||||
enable = true;
|
||||
config = ''
|
||||
[sssd]
|
||||
domains = az-group.local
|
||||
config_file_version = 2
|
||||
services = nss, pam
|
||||
|
||||
[domain/az-group.local]
|
||||
id_provider = ad
|
||||
auth_provider = ad
|
||||
access_provider = ad
|
||||
chpass_provider = ad
|
||||
ad_domain = ${domain}
|
||||
ad_server = az-dc01.az-group.local, az-dc02.az-group.local
|
||||
krb5_realm = ${realm}
|
||||
krb5_server = az-dc01.az-group.local, az-dc02.az-group.local
|
||||
krb5_keytab = /etc/krb5.keytab
|
||||
krb5_store_password_if_offline = True
|
||||
cache_credentials = True
|
||||
use_fully_qualified_names = false
|
||||
fallback_homedir = /home/%u
|
||||
override_shell = /run/current-system/sw/bin/bash
|
||||
default_shell = /run/current-system/sw/bin/bash
|
||||
ad_gpo_access_control = permissive
|
||||
enumerate = true
|
||||
ldap_id_mapping = false
|
||||
'';
|
||||
};
|
||||
|
||||
# DNS — Thin Clients use AD DCs as resolver (NetBird DNS overlays
|
||||
# for corp-VPN-only domains via systemd-resolved; see network/dns.nix)
|
||||
networking.nameservers = lib.mkDefault ["az-dc01.az-group.local" "az-dc02.az-group.local"];
|
||||
networking.domain = lib.mkDefault domain;
|
||||
networking.search = lib.mkDefault [domain];
|
||||
|
||||
# Host FQDN — important for Kerberos SPN matching.
|
||||
# `networking.hostName` is set by the host's default.nix; here we
|
||||
# only add the hosts file fallback so the FQDN resolves locally even
|
||||
# before DNS is reachable.
|
||||
networking.extraHosts = ''
|
||||
127.0.0.1 ${hostname}.${domain} ${hostname}
|
||||
'';
|
||||
|
||||
# agenix-deployed machine keytab
|
||||
age.secrets."${hostname}-krb5-keytab" = {
|
||||
file = ../../secrets/${hostname}-krb5-keytab.age;
|
||||
path = "/etc/krb5.keytab";
|
||||
mode = "0600";
|
||||
owner = "root";
|
||||
group = "root";
|
||||
};
|
||||
|
||||
# Ensure keytab path is readable by sssd (runs as root) but not by
|
||||
# anyone else.
|
||||
systemd.services.sssd = {
|
||||
after = ["age-identity.service"];
|
||||
wants = ["age-identity.service"];
|
||||
serviceConfig.ExecStartPre = let
|
||||
check = pkgs.writeShellScript "check-keytab" ''
|
||||
if [ ! -s /etc/krb5.keytab ]; then
|
||||
echo "ERROR: /etc/krb5.keytab is empty or missing." >&2
|
||||
echo "Provision via adcli join + agenix, see roles/thin-client/README.md." >&2
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
in ["+${check}"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# roles/thin-client/identity/default.nix
|
||||
#
|
||||
# Identity layer: Active Directory integration (realm/sssd/krb5 via keytab),
|
||||
# local break-glass users (without m3ta-home), sudo policy.
|
||||
#
|
||||
# All AD-related secrets are provisioned via agenix:
|
||||
# secrets/<hostname>-krb5-keytab.age → /etc/krb5.keytab
|
||||
#
|
||||
# See README.md for the pre-create workflow.
|
||||
{config, lib, ...}: {
|
||||
imports = [
|
||||
./ad.nix
|
||||
./local-users.nix
|
||||
./sudo.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
# roles/thin-client/identity/local-users.nix
|
||||
#
|
||||
# Local break-glass users for Thin Clients. These exist independently of
|
||||
# AD/DNS/NetBird availability. Used for disaster recovery via NetBird SSH.
|
||||
#
|
||||
# NOTE: This intentionally does NOT import the m3ta-home profile system
|
||||
# from hosts/common/users/. Thin Clients are not dev machines and don't
|
||||
# need home-manager profiles — just a lean account with password, SSH key,
|
||||
# and wheel membership.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
inherit (lib) mkIf;
|
||||
cfg = config.az.tc;
|
||||
in {
|
||||
config = mkIf cfg.enable {
|
||||
users.users."sascha.koenig" = {
|
||||
# Re-uses the existing hashedPassword from hosts/common/users/sascha.koenig.nix
|
||||
hashedPassword = "$y$j9T$ORX4btVZgs9Xjq2oIvzJm0$lXiPwaa0D6t.eMDIx1UBesEAMOkWXBoGwpeI7X0aS8D";
|
||||
isNormalUser = true;
|
||||
shell = config.users.defaultUserShell;
|
||||
extraGroups = ["wheel" "networkmanager" "plugdev" "input"];
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZbg/Z9mnflXuLahGY8WOSBMqbgeqVIkIwRkquys1Ml sascha.koenig@azintec.com"
|
||||
];
|
||||
};
|
||||
|
||||
users.users."jannik.mueller" = {
|
||||
# Re-uses the existing hashedPassword from hosts/common/users/jannik.mueller.nix
|
||||
hashedPassword = "$y$j9T$09RgD3AU3PK9Oi6JGLe0V1$i8J2ZOD1h1b6Zpw28ub.kExujoDKHzokeXzkM23Tfd/";
|
||||
isNormalUser = true;
|
||||
shell = config.users.defaultUserShell;
|
||||
extraGroups = ["wheel" "networkmanager" "plugdev" "input"];
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPvZazSuIoWoRWhkAqQDMLeurxVUyy1MTllp1wfw1tzq"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# roles/thin-client/identity/sudo.nix
|
||||
#
|
||||
# Sudo policy:
|
||||
# - Members of "domain admins" may sudo WITH their password.
|
||||
# - Local users in wheel (sascha.koenig, jannik.mueller) may sudo WITH
|
||||
# their password.
|
||||
# - Normal domain users may NOT sudo (everything they need — USB mount,
|
||||
# audio, screen-lock — runs via polkit/udisks).
|
||||
#
|
||||
# Q7 decision: "Ja, aber mit Passwort" — passwordless sudo is disabled.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
inherit (lib) mkIf;
|
||||
cfg = config.az.tc;
|
||||
in {
|
||||
config = mkIf cfg.enable {
|
||||
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"];
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user