# 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" \ # @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= --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}"]; }; }; }