From f4bbd6331d772fe51c599efd398ae404febd335d Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Mon, 27 Jul 2026 23:31:21 +1000 Subject: [PATCH 1/5] feat(ipa): add reusable declarative FreeIPA client module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds modules/ipa/client.nix — a parameterized module that joins a NixOS host to the sweet.home FreeIPA domain without ipa-client-install. It configures security.ipa (SSSD, Kerberos, PAM, NSSwitch) and places a pre-provisioned host keytab via sops-nix binary secret so enrollment is fully reproducible from the flake. - variables.nix: adds ipaServer (FQDN of the FreeIPA KDC; security.ipa.server requires a hostname, not an IP, for Kerberos/TLS) - certs/ipa-ca.crt: placeholder for the IPA CA public certificate (operator replaces with: curl http:///ipa/config/ca.crt) - secrets/nix-cache.keytab: placeholder binary sops file (operator replaces with the encrypted keytab after ipa host-add + ipa-getkeytab) - .sops.yaml: adds creation rule for secrets/nix-cache.keytab (same recipients as secrets/nix-cache.yaml) - hosts/nix-cache/host.nix: imports the IPA client module; adds networking.domain so the host's FQDN resolves correctly Module header documents the three operator steps needed per host before deploy. Co-Authored-By: Claude Sonnet 4.6 --- .sops.yaml | 10 ++++++ certs/ipa-ca.crt | 9 +++++ hosts/nix-cache/host.nix | 5 +++ modules/ipa/client.nix | 73 ++++++++++++++++++++++++++++++++++++++++ secrets/nix-cache.keytab | 0 variables.nix | 1 + 6 files changed, 98 insertions(+) create mode 100644 certs/ipa-ca.crt create mode 100644 modules/ipa/client.nix create mode 100644 secrets/nix-cache.keytab diff --git a/.sops.yaml b/.sops.yaml index a14898b..462abf5 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -63,6 +63,16 @@ creation_rules: - *lxc-nix-cache - *proxmox-nix-cache + # Host keytab for nix-cache FreeIPA enrollment (binary sops file). + # Generate with: sops -e --input-type binary /tmp/nix-cache.keytab > secrets/nix-cache.keytab + - path_regex: secrets/nix-cache\.keytab$ + key_groups: + - age: + - *admin + - *linode-nix-cache + - *lxc-nix-cache + - *proxmox-nix-cache + - path_regex: secrets/server\.yaml$ key_groups: - age: diff --git a/certs/ipa-ca.crt b/certs/ipa-ca.crt new file mode 100644 index 0000000..1894658 --- /dev/null +++ b/certs/ipa-ca.crt @@ -0,0 +1,9 @@ +# Placeholder — replace with the actual FreeIPA CA certificate before deploying. +# +# Retrieve from the IPA server (it is a public certificate, safe to commit): +# curl -o certs/ipa-ca.crt http:///ipa/config/ca.crt +# +# This file must contain a valid PEM certificate for SSSD to authenticate +# against FreeIPA over TLS. The Nix build succeeds with a placeholder, but +# the deployed host will not be able to join the domain until the real cert +# is committed and the system is rebuilt. diff --git a/hosts/nix-cache/host.nix b/hosts/nix-cache/host.nix index 89f6c55..b9b2c0f 100644 --- a/hosts/nix-cache/host.nix +++ b/hosts/nix-cache/host.nix @@ -6,10 +6,15 @@ name = "nix-cache"; sopsFile = ../../secrets/nix-cache.yaml; }) + (import ../../modules/ipa/client.nix { + keytabSopsFile = ../../secrets/nix-cache.keytab; + caCertFile = ../../certs/ipa-ca.crt; + }) ]; networking = { hostName = vars.nixCacheHost; + domain = vars.homeDomain; useDHCP = false; interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{ address = vars.nixCacheIp; diff --git a/modules/ipa/client.nix b/modules/ipa/client.nix new file mode 100644 index 0000000..bf56ae3 --- /dev/null +++ b/modules/ipa/client.nix @@ -0,0 +1,73 @@ +# Fully declarative FreeIPA domain membership. +# +# Configures security.ipa (SSSD, Kerberos, PAM, NSSwitch) and places a +# pre-provisioned host keytab via sops-nix so no imperative ipa-client-install +# step is needed after deployment. +# +# Usage (in a host.nix imports list): +# (import ../../modules/ipa/client.nix { +# keytabSopsFile = ../../secrets/nix-cache.keytab; +# caCertFile = ../../certs/ipa-ca.crt; +# }) +# +# One-time operator setup per host (do this BEFORE deploying): +# +# 1. Fetch the IPA CA certificate (public — safe to commit): +# curl -o certs/ipa-ca.crt http:///ipa/config/ca.crt +# Replace the placeholder at certs/ipa-ca.crt and commit it. +# +# 2. On the FreeIPA server, add the host and generate a keytab: +# ipa host-add --ip-address= +# ipa-getkeytab -s -p host/ -k /tmp/.keytab +# +# 3. sops-encrypt the keytab as a binary secret from your admin machine: +# sops -e --input-type binary /tmp/.keytab \ +# > secrets/.keytab +# Add secrets/.keytab to .sops.yaml with the host's age key as a +# recipient (see the nix-cache.keytab entry for the pattern), then run: +# scripts/secrets/sync-host-keys.sh # if not done yet +# sops updatekeys secrets/.keytab +# Commit the encrypted file. +# +# 4. nixos-rebuild switch (or create-proxmox-resource.sh) — no further +# manual enrollment steps required. +# +# vars dependencies: homeDomain, ipaServer + +{ keytabSopsFile, caCertFile }: +{ config, lib, pkgs, vars, ... }: + +let + realm = lib.strings.toUpper vars.homeDomain; + fqdn = "${config.networking.hostName}.${vars.homeDomain}"; + # "sweet.home" -> "dc=sweet,dc=home" + basedn = lib.strings.concatMapStringsSep "," (c: "dc=${c}") (lib.strings.splitString "." vars.homeDomain); + # security.ipa.certificate expects a derivation (package), not a raw path. + caCertPkg = pkgs.writeText "ipa-ca.crt" (builtins.readFile caCertFile); +in +{ + security.ipa = { + enable = true; + domain = vars.homeDomain; + realm = realm; + server = vars.ipaServer; + certificate = caCertPkg; + basedn = basedn; + ipaHostname = fqdn; + offlinePasswords = true; + cacheCredentials = true; + }; + + # Host keytab: pre-provisioned on the IPA server, sops-encrypted binary. + # Placed at /etc/krb5.keytab before SSSD starts so the host authenticates + # to IPA without running ipa-client-install. + sops.secrets."ipa-host-keytab" = { + sopsFile = keytabSopsFile; + format = "binary"; + path = "/etc/krb5.keytab"; + owner = "root"; + group = "root"; + mode = "0600"; + restartUnits = [ "sssd.service" ]; + }; +} diff --git a/secrets/nix-cache.keytab b/secrets/nix-cache.keytab new file mode 100644 index 0000000..e69de29 diff --git a/variables.nix b/variables.nix index ec50ec2..6ebc824 100644 --- a/variables.nix +++ b/variables.nix @@ -17,6 +17,7 @@ dockerIp = "192.168.2.225"; # docker Proxmox VM LAN IP pbsIp = "192.168.2.244"; # Proxmox Backup Server LAN IP (not NixOS-managed) domainControllerIp = "192.168.2.253"; # FreeIPA domain controller / primary DNS (not NixOS-managed) + ipaServer = "ipa.sweet.home"; # FreeIPA server hostname (used by security.ipa and Kerberos; must be a resolvable FQDN, not an IP — update if different) # Cross-host references (LAN hostnames/users other hosts reach over the network) nixCacheHost = "nix-cache"; # substituter/remote-builder hostname From a4b49c990993fcf69d27b47c49e0211169fe6a11 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 07:16:53 +1000 Subject: [PATCH 2/5] fix(ipa): correct ipaServer to domain-controller.sweet.home ipa-getkeytab confirmed ipa.sweet.home doesn't respond to LDAP; domain-controller.sweet.home is the actual IPA server FQDN. Co-Authored-By: Claude Sonnet 4.6 --- variables.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variables.nix b/variables.nix index 6ebc824..e438178 100644 --- a/variables.nix +++ b/variables.nix @@ -17,7 +17,7 @@ dockerIp = "192.168.2.225"; # docker Proxmox VM LAN IP pbsIp = "192.168.2.244"; # Proxmox Backup Server LAN IP (not NixOS-managed) domainControllerIp = "192.168.2.253"; # FreeIPA domain controller / primary DNS (not NixOS-managed) - ipaServer = "ipa.sweet.home"; # FreeIPA server hostname (used by security.ipa and Kerberos; must be a resolvable FQDN, not an IP — update if different) + ipaServer = "domain-controller.sweet.home"; # FreeIPA server hostname (used by security.ipa and Kerberos; must be a resolvable FQDN, not an IP) # Cross-host references (LAN hostnames/users other hosts reach over the network) nixCacheHost = "nix-cache"; # substituter/remote-builder hostname From 8955840f0a2893dd932b19e322b7340cbf80218a Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 07:19:53 +1000 Subject: [PATCH 3/5] fix(ipa): use in-place sops encryption in module docs sops matches creation rules against the input file path, so encrypting /tmp/.keytab directly with stdout redirect fails to find the rule. Copy to secrets/ first, then use -i to encrypt in-place. Co-Authored-By: Claude Sonnet 4.6 --- modules/ipa/client.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/ipa/client.nix b/modules/ipa/client.nix index bf56ae3..db83042 100644 --- a/modules/ipa/client.nix +++ b/modules/ipa/client.nix @@ -20,9 +20,11 @@ # ipa host-add --ip-address= # ipa-getkeytab -s -p host/ -k /tmp/.keytab # -# 3. sops-encrypt the keytab as a binary secret from your admin machine: -# sops -e --input-type binary /tmp/.keytab \ -# > secrets/.keytab +# 3. sops-encrypt the keytab as a binary secret from your admin machine +# (must run from repo root; sops matches creation rules against the file +# path, so copy to secrets/ first and encrypt in-place): +# cp /tmp/.keytab secrets/.keytab +# sops -e --input-type binary -i secrets/.keytab # Add secrets/.keytab to .sops.yaml with the host's age key as a # recipient (see the nix-cache.keytab entry for the pattern), then run: # scripts/secrets/sync-host-keys.sh # if not done yet From b4fb9c25f2af77890fe242fe9c0bf2f24be13456 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 07:25:25 +1000 Subject: [PATCH 4/5] secrets(nix-cache): add sops-encrypted IPA host keytab --- secrets/nix-cache.keytab | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/secrets/nix-cache.keytab b/secrets/nix-cache.keytab index e69de29..65aa101 100644 --- a/secrets/nix-cache.keytab +++ b/secrets/nix-cache.keytab @@ -0,0 +1,26 @@ +{ + "data": "ENC[AES256_GCM,data:z2bazlADvtWkGcY4an7NgZgIDSDq8KmYpSq8tcBleW/33HlElMNhdmje8m8hd7WxgrVvCQDdlAfodBLj2mjbgSwBptlPX2zbwcmJdm0g0Ope+YyQZucIio7NnBaYa+OK8meWqmxO2WEeOk+hnNfNPbt8MkzF/FMMbP8xv9wSTKP9CDdmO7s8rdg8G1JxVfn9dyaSDUTuA2JGNtbh+osd4CtNCV/bc84MjXDZTgTfT0W/uv8VH3iPNT7BhQbU9FIY4dsFRYBf758VtJ+3mTBiQkR38IevJBvhFB7Wiqn118LSBgjdTKJnBwmGo6lMdo0LRYHEgUnb2ejhWMNie19x+LEAErDki0RBmjc2BbnOF908kUlW2fUPo5fz/hlfqlhDLS1uOxESKhfnKt7s5qzWiMX/nohpknVfyMbmu9j/M571z9sgFnJAylpW3NLp53lX8N9mINwRGXsyL/ijbNE=,iv:M/MNquAGN+lIIyVVvFRgKR6DKAhLclc/OY1HV5Xwoog=,tag:eEuURMErSqsoh+l9ZaHY9Q==,type:str]", + "sops": { + "age": [ + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqZlUxRG0zR3IzMlpDL3Vl\nNmRWQXUvc1NDek5wbWtKVEFTcTlxL3ZoWldNCmJVMkFtbDkxN1FGQ2VLTlhBUW5Q\nZ3VzL0UydUQ2YnlUMmtqa1Z0d0FSVFUKLS0tIGJFelc4Rm84T3ZHSGZDdnNUbUFM\nRXZHV3N3aVlSOXhUZk04UFRMWU5TbFUKhPU80PVYuDFUCxu1CA8+W8bqkr+Ne2fh\n+nBUPJbGxfN9TyD9tUC77AMbcL1R2L5x+SSAh0bEgSWE24/RjsuVKw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3M1o3S2FibHV6N2tIOFEx\nRytJRDFIUU05azNDbWlpaDdFQXFRWlBUNnlFCnBwNVB0SEVwL3NWNS9aeG4xSUI1\nV3NOcUFxUUhaODYyWXdFMEhWeFpvancKLS0tIHY4NHFQRGJwUURQbWZ2cGlSdm9s\nOFJodGZkekk3UnBRRUwxb2YrQ1ZPcU0K0tCapb1hfVHFSNpmESXexYa5k9OE9Tha\n51QpU4mZHKGrnWPK/kyj7rHiz95TLsmwUdA8Q2hOiYNSxEaVBjYDQw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1jcx3yajjhghn8qh8za3yeu8nxykzlg3p4nrv03vnfvzl0mzayg2qmg940e" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkTmNrbUFoVFVGcytzQkk1\nMUxDdmJGL0xDcUNZaDFRQnUwNXoyMU90SEVvCi9EaTlVOGlMeUZtQzRJS1J2TTJY\neWdkMGNFMWZzQXVKOU5KcXVvalk1ekkKLS0tIDN2MlRQcG5aQnN3bUMrSDdvc09X\nM29hQ0pJRzk1amRHaS9mRlhrb2FMZG8KEWkSSP+MqGRU75qo7ctOCL7qHhyCM3l4\nL+ga1XOKxRsQkKQPozCi5Bm+k28W9hIDaC34Rw9difxICM9Z1rAbOA==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1ufg390ydrmma849t9xfkxxl5xvdkk6mngnlzhmy7mvuaje8sgcmsmnq6l7" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1TUtxVjJ2STFpU3RNdWpJ\nVjcwUHVORVN6L1pKVC9TeWt4cTNoR2pPWVRBCkR4QVJ1bHFUa2Z5aWw1bG0vQWVy\ncjNGRDl5REZzbzdZWTFMRkU1eHFTc1kKLS0tIHNLVDFiRkRKTHhrSEx2S0J1R3cx\nSkdEQWJya1ptTTRqUjJZT3FFaFhkd0kKgecHrnzW9+Eb+b7c0z1yR+Y0Czsr9Kjh\nx1UUOfleHntJUCs8oYcOogKknEnhBJJoJ5oe/gWruRSwle1fs3cBqQ==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1jlltcv5jcnm40z5k0q6hv053k2rqpqvemtuecdwn527uw8uqz4es3x7m68" + } + ], + "lastmodified": "2026-07-27T21:22:58Z", + "mac": "ENC[AES256_GCM,data:VG2ygV4X6yMxGlJgq3sN4GAzgVqiD06noyMxB2yb/FCmiFVYZAH/9LqF//G7MTInfIjFrTDyoHAny+m/ztUrJkHXI1Fzg0U8R/zYyQ6wj/GguL0gyhA70uriQhvHsRHefHPa7Km61Blo9cME6CBJWejtRvh1IMw9itAioVrSIZE=,iv:zjxNG6IYxkcNqLk08NADoDiIbS6at86y136SBzadW1U=,tag:sj4ZRR7ixU8h7aOUJmTCYg==,type:str]", + "version": "3.13.2" + } +} From f2f0fcf756dd24123b22fd791bad4e67a15ad319 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 07:25:28 +1000 Subject: [PATCH 5/5] certs: add FreeIPA CA certificate --- certs/ipa-ca.crt | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/certs/ipa-ca.crt b/certs/ipa-ca.crt index 1894658..614814a 100644 --- a/certs/ipa-ca.crt +++ b/certs/ipa-ca.crt @@ -1,9 +1,25 @@ -# Placeholder — replace with the actual FreeIPA CA certificate before deploying. -# -# Retrieve from the IPA server (it is a public certificate, safe to commit): -# curl -o certs/ipa-ca.crt http:///ipa/config/ca.crt -# -# This file must contain a valid PEM certificate for SSSD to authenticate -# against FreeIPA over TLS. The Nix build succeeds with a placeholder, but -# the deployed host will not be able to join the domain until the real cert -# is committed and the system is rebuilt. +-----BEGIN CERTIFICATE----- +MIIESDCCArCgAwIBAgIBATANBgkqhkiG9w0BAQsFADA1MRMwEQYDVQQKDApTV0VF +VC5IT01FMR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjYwNzI2 +MjExMzQxWhcNNDYwNzI2MjExMzQxWjA1MRMwEQYDVQQKDApTV0VFVC5IT01FMR4w +HAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggGiMA0GCSqGSIb3DQEBAQUA +A4IBjwAwggGKAoIBgQCzljYktbHdMGVJ6Wq0XQJuHLN6dkCSOgtoIzQtriPQkkNI +uo28LwobaiQQ8sX4kGRH/BTKnH8QlId/jug4Uc+sDHnABYu++AiOhPbBX8gCpRQ0 +hebBjZiktHSBUEJR31siWOVdBoKBDJEoxehx7XUXvcxIJcaRN+LHYjO86nJN55HB +VwFU2JcYDk98c+144dFJxXdr++MjWe4Z/oVVU8JHIOtNtKhVhvij6oOSWxcYoJO/ +S80LRj1vx/o6o/3G6bYug7PjY7JjZk/Oj61whijZkcsoO1MXSYI6UywJZGflv+ZB +7HyufdYAsK3WhE8O2FX3/kq64Ol83HNtoR8Dt68rTg1xpW6K45jS6iDPKueYGkb0 +oSx7e++90VAW2PDhj6QQ3JJ4O5VQwrrecekJzUrAean0FOEbmgyi4PsEp1Vk6LDQ +SsIn1x0euyxVivQMlzNX2XrZL3urn1BNPqAdntXQMkR0Wl8sbUiJPe0kxG52CGXs +6yfNEXbPmVGcC0TBdGECAwEAAaNjMGEwHQYDVR0OBBYEFLh5QbI1UWMH0WR4z8bG +lhrOX3X5MB8GA1UdIwQYMBaAFLh5QbI1UWMH0WR4z8bGlhrOX3X5MA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgHGMA0GCSqGSIb3DQEBCwUAA4IBgQCVodVN +owwo53OQe02QhtEbIur2PL7zIfvhvCTRD4J8gwpbMIqT7JQK0tV6Mvsg2L8yTb2O +KjrWeLKHGWaZZlhGSPTbkMFdb/Ls8M9FSnkc2bwcdWW3Z1lOiCjBYYqwLCG6JhvB +5SXVwWNJwXeasL2m7oFTSwhsqPpARJ2t25u2N35o+tqIoCjijKwkmEOT66N9EAbu +2VQjtYZWPkBtP4YCe0Ey6u4oy7sy8ThNAjOylZok+J4JW7QEFjK4Q/emhA4aQq5H +gg9qgMuG+5oi6D1g2Wy+fMTRBaukJtLYZbBpQMQhMYWg44uPp/2bbNPTID/nV1KB +GcPyHaskcVxPdYWxAPMwk3AeJXWyOq7atAPTF5sbk0kQQf2m+vyOqcli5CxRMUgV +rcyi9l6+dZW4U+38Q0ET5M3OuxNI4hA7kVY2cfTakXWNqh97+TIHnstblDhAxECK +6ZLMJQYUy7LqJTX84H27CBWLexEMjXwdr5HCV88Fj6mAK0fRufnIw5FeneA= +-----END CERTIFICATE-----