From 2f829ba3e749966b0e6f944ac48f32bf2eb24295 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 11:15:59 +1000 Subject: [PATCH] refactor(ipa): move FreeIPA client to common config with auto-enrollment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit modules/ipa/client.nix is now a self-contained NixOS module (no longer a parameterized function): it checks builtins.pathExists for secrets/.keytab at eval time and enables itself automatically if found, making it a no-op for hosts without a keytab. modules/common/configuration.nix imports it so every host in the flake is a candidate for IPA enrollment — no per-host wiring needed. Adding a keytab (via scripts/ipa/create-nixos-ipa-host-account.sh) is now the only step required to enroll a host. The module also sets networking.domain and networking.nameservers via mkDefault when active, so new hosts don't need those set explicitly. Also: - Remove explicit IPA imports from hosts/nix-cache and hosts/tailscale-router - Add secrets/pxe-boot.keytab + creation rule; remove incorrect secrets/nixos.sweet.home.keytab and its creation rule - Add .sops.yaml creation rules for all remaining host keytabs (server, docker, tor-relay, nix-minimal, nixos) so the creation script can target them without manual .sops.yaml edits - Fix duplicate tailscale-router.keytab rule and corrupted gui.yaml comment block in .sops.yaml Co-Authored-By: Claude Sonnet 4.6 --- .sops.yaml | 85 ++++++++++++++++++++++---------- hosts/nix-cache/host.nix | 5 -- hosts/tailscale-router/host.nix | 5 -- modules/common/configuration.nix | 1 + modules/ipa/client.nix | 50 +++++++------------ secrets/nixos.sweet.home.keytab | 14 ------ secrets/pxe-boot.keytab | 22 +++++++++ 7 files changed, 100 insertions(+), 82 deletions(-) delete mode 100644 secrets/nixos.sweet.home.keytab create mode 100644 secrets/pxe-boot.keytab diff --git a/.sops.yaml b/.sops.yaml index d9062b0..c62ba58 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -63,16 +63,6 @@ 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: @@ -127,16 +117,6 @@ creation_rules: # scripts/secrets/sync-host-keys.sh yet, so whichever variant is actually # deployed next needs its recipient added here (and `sops updatekeys` rerun) # before it can decrypt this. - # Host keytab for tailscale-router FreeIPA enrollment (binary sops file). - # Generated by scripts/ipa/create-nixos-ipa-host-account.sh. - - path_regex: secrets/tailscale-router\.keytab$ - key_groups: - - age: - - *admin - - *lxc-tailscale-router - - *proxmox-tailscale-router - - *linode-tailscale-router - - path_regex: secrets/gui\.yaml$ key_groups: - age: @@ -146,19 +126,72 @@ creation_rules: - *linode-gui - *proxmox-gui - # Host keytab for tailscale-router FreeIPA enrollment (binary sops file). - # Generated by scripts/ipa/create-nixos-ipa-host-account.sh. + # IPA host keytabs (binary sops files). + # Each keytab is encrypted for all platform variants of that host so any + # deployed variant can decrypt it at boot. Run + # scripts/ipa/create-nixos-ipa-host-account.sh to enroll a new + # host and produce the keytab; this section is updated by that script. + + - path_regex: secrets/nix-cache\.keytab$ + key_groups: + - age: + - *admin + - *linode-nix-cache + - *lxc-nix-cache + - *proxmox-nix-cache + - path_regex: secrets/tailscale-router\.keytab$ key_groups: - age: - *admin + - *linode-tailscale-router - *lxc-tailscale-router - *proxmox-tailscale-router - - *linode-tailscale-router - # Host keytab for nixos.sweet.home FreeIPA enrollment (binary sops file). - # Generated by scripts/ipa/create-nixos-ipa-host-account.sh. - - path_regex: secrets/nixos.sweet.home\.keytab$ + - path_regex: secrets/pxe-boot\.keytab$ key_groups: - age: - *admin + - *lxc-pxe-boot + - *proxmox-pxe-boot + + # nixos = the workstation (hosts/nixos/host.nix). All gui platform variants + # share the hostname "nixos" and must be able to decrypt at boot. + - path_regex: secrets/nixos\.keytab$ + key_groups: + - age: + - *admin + - *baremetal-gui + - *lxc-gui + - *proxmox-gui + - *linode-gui + + - path_regex: secrets/server\.keytab$ + key_groups: + - age: + - *admin + - *linode-server + - *lxc-server + - *proxmox-server + + - path_regex: secrets/docker\.keytab$ + key_groups: + - age: + - *admin + - *linode-docker + - *lxc-docker + - *proxmox-docker + + - path_regex: secrets/tor-relay\.keytab$ + key_groups: + - age: + - *admin + - *lxc-tor-relay + + - path_regex: secrets/nix-minimal\.keytab$ + key_groups: + - age: + - *admin + - *lxc-minimal + - *proxmox-minimal + - *linode-minimal diff --git a/hosts/nix-cache/host.nix b/hosts/nix-cache/host.nix index b9b2c0f..89f6c55 100644 --- a/hosts/nix-cache/host.nix +++ b/hosts/nix-cache/host.nix @@ -6,15 +6,10 @@ 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/hosts/tailscale-router/host.nix b/hosts/tailscale-router/host.nix index c95440c..6198bf4 100644 --- a/hosts/tailscale-router/host.nix +++ b/hosts/tailscale-router/host.nix @@ -6,15 +6,10 @@ name = "tailscale-router"; sopsFile = ../../secrets/tailscale-router.yaml; }) - (import ../../modules/ipa/client.nix { - keytabSopsFile = ../../secrets/tailscale-router.keytab; - caCertFile = ../../certs/ipa-ca.crt; - }) ]; networking = { hostName = "tailscale-router"; - domain = vars.homeDomain; useDHCP = false; interfaces.${vars.lxcLanInterface}.ipv4.addresses = [{ address = vars.tailscaleRouterIp; diff --git a/modules/common/configuration.nix b/modules/common/configuration.nix index eba6347..68fa68e 100644 --- a/modules/common/configuration.nix +++ b/modules/common/configuration.nix @@ -6,6 +6,7 @@ # Include the results of the hardware scan. # ./hardware-configuration.nix ./set-locale.nix + ../ipa/client.nix ]; # Use the GRUB 2 boot loader. # boot.loader.grub.enable = true; diff --git a/modules/ipa/client.nix b/modules/ipa/client.nix index 2cec678..a5703f9 100644 --- a/modules/ipa/client.nix +++ b/modules/ipa/client.nix @@ -1,34 +1,15 @@ # 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. +# Imported by modules/common/configuration.nix — no per-host wiring needed. +# Enables itself automatically on any host that has a sops-encrypted keytab +# at secrets/.keytab; is a no-op for all other hosts. # -# Usage (in a host.nix imports list): -# (import ../../modules/ipa/client.nix { -# keytabSopsFile = ../../secrets/.keytab; -# caCertFile = ../../certs/ipa-ca.crt; # already committed — do not re-fetch -# }) -# -# The host.nix networking block must also set: -# networking.domain = vars.homeDomain; # needed for Kerberos FQDN -# networking.nameservers = [ vars.domainControllerIp ]; # IPA DNS -# -# One-time operator setup per host (do this BEFORE deploying): -# -# 0. Generate SSH host keys and the host's age key for sops: -# scripts/secrets/sync-host-keys.sh -# This must run before step 1 so the host age key is in .sops.yaml -# and the keytab can be encrypted for the host to read at boot. -# -# 1. Add the IPA host account and produce the sops-encrypted keytab: -# scripts/ipa/create-nixos-ipa-host-account.sh [--ip ] -# The script handles ipa host-add, ipa-getkeytab, .sops.yaml patching, -# and sops encryption in one step. See the script header for details. -# -# 2. Wire up the host (see "Usage" above), then deploy: -# nixos-rebuild switch (or create-proxmox-resource.sh) -# No further manual enrollment steps are required after deployment. +# To enroll a new host: +# 0. scripts/secrets/sync-host-keys.sh +# 1. scripts/ipa/create-nixos-ipa-host-account.sh [--ip ] +# (adds .sops.yaml rule, runs ipa host-add, encrypts keytab in one step) +# 2. git add secrets/.keytab .sops.yaml && git commit +# 3. Deploy — no further steps required. # # Manual fallback (if the script isn't usable): # a. On the FreeIPA server: ipa host-add [--ip-address=] --force @@ -40,18 +21,23 @@ # # vars dependencies: homeDomain, ipaServer, domainControllerIp -{ keytabSopsFile, caCertFile }: { config, lib, pkgs, vars, ... }: let + keytabPath = ../../secrets + "/${config.networking.hostName}.keytab"; + enabled = builtins.pathExists keytabPath; + 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); + caCertPkg = pkgs.writeText "ipa-ca.crt" (builtins.readFile ../../certs/ipa-ca.crt); in -{ +lib.mkIf enabled { + networking.domain = lib.mkDefault vars.homeDomain; + networking.nameservers = lib.mkDefault [ vars.domainControllerIp ]; + security.ipa = { enable = true; domain = vars.homeDomain; @@ -92,7 +78,7 @@ in # 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; + sopsFile = keytabPath; format = "binary"; path = "/etc/krb5.keytab"; owner = "root"; diff --git a/secrets/nixos.sweet.home.keytab b/secrets/nixos.sweet.home.keytab deleted file mode 100644 index a545b51..0000000 --- a/secrets/nixos.sweet.home.keytab +++ /dev/null @@ -1,14 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:dY+YT+OIM3yk9MSfTEiiHQuBP12fLn2txqK2W4p2uLWxc/KY1wbvIwNE7ug2S27NH3w3jLqvlAIfII4pKxujNEQIZ7ylX+2J/F7finn6G8QnTJMFj20JQo7TSDOOOHcOsmjtb6C/07qCPjOUxQ58SiYH2+qDf5DbniNldcFGpWDkw9DbrYJ6G3Ugnfl5D6ZSCNjBjmTavEDFW9GPLBgmseaDycIung7Rz3MjNEx31LVTBnlztweqoQhpOTqWcLiXK+4YnVaNAiWyNOpFXeronWNU+fItntp5TxJInUdbQioZJ6+qUjk3ZHxuikKuGjEpORTeHptX8m3AxenPiKEvDzLuRN6lnGzcbuH8+DvKIwA06QUeLRYARAnbGujkfS24i2Zkm+HQq0ZK5Y/0d4OoHi9dX5YQo39mCAUJxN/+R3T4QdMVLi+eDnkbNNCwlsRyivhwl64SjGMvFq1uXl6+wGTQ0/Aj16dJR273l+WsSSu509Q0sVdwbE0u,iv:UnpS8KFuNVtdYa/eCMWtQlm9awwNDgMU3Cy5x7Vfycc=,tag:NlGOfcg59NdwXJJesTZ1/w==,type:str]", - "sops": { - "age": [ - { - "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3ZEdXUEF4WWhPTGFFcWkr\nV3ZsVjl2SWQ0V3VjeHdjVmJ5ZnRGcExPN0ZzCjBZbTNVYWlqd0NnRHZmSUpObC9s\nUUhJQkp1em9YeGxGaHN3ZjE5eWFPSEEKLS0tIDdOSWU3b1pRSzBxMDhIWjJ4SDRO\nRVA5S3FVSFZGRm5XejdlQytCL2k3NmcKFeQ/nRcuEiFwgJEDj8z0KMeyEcFGbG35\nBcuM5cUh5qLArOQlONThmOt/Jepfo2dMPblQ6kd0zSqWfLNqZDg3Hw==\n-----END AGE ENCRYPTED FILE-----\n", - "recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad" - } - ], - "lastmodified": "2026-07-28T00:42:31Z", - "mac": "ENC[AES256_GCM,data:IiLoY24bVjvbsA3bd9a6/OEw0PDkHm1Uc3OiHG2FlOxAoPhb6zvE5t2tpZyqeT2goIYv+mr4C/y8BYSsVcZr1FtgqwYx7Y5eiAq9uVzFQ838TDN5ndgkKYrbyPwHSKCHCrOPZ/jDiQTSSO/dv1R8cQHfBf8kX5GEt6ovtGvI2AY=,iv:PctMMNSv0iPZYATsACcQ0aqgFME7xGGCD915k/RiuEI=,tag:Pxaet5zPXIYicExNRFyzGA==,type:str]", - "version": "3.13.2" - } -} diff --git a/secrets/pxe-boot.keytab b/secrets/pxe-boot.keytab new file mode 100644 index 0000000..fd63acd --- /dev/null +++ b/secrets/pxe-boot.keytab @@ -0,0 +1,22 @@ +{ + "data": "ENC[AES256_GCM,data:Q++XWxg9tvY7ugT8+8FWCC5jgOfQ1+LYLsnqN0/WGxTf5XT2VmoXvszcE/ow2BUOBG3qBTXS3OHYFPwmj1GgaBHB8rpdXX6+LveSBE2gmx1VR+NUDTxy+0/DBq411MK9n/J9eIYcybdFIE11biFSAob9EgfxBF5roCDXIPDPyVCSe6LyhNvqYPnGQsfHCbejSewLTcRQEiguP9BX96CpMPIpmaB9fHN25t5RWCgMI7MtacrRxRsyKg44+2FZstXdZrp2Wv9u86BxqdAFqtZE8qpPeGdrdzluZx9jhnw0wZPzHdKg5wS7/UrLCb0UxIQiDxDMDuuBuLGkKXfAhb6SGZU41wWAWYk548iGRUaG+79BO4HhRZNObOFvfsjpMXEfcH/Vbv/wHVY4OpJUPe/ZWK4wpL9YrY4nT0t62HH7MirOy1uhwLE50h2+6dHKj6ur5G7thkSqgzVWXQ==,iv:zaRBwS+gfXLhH30havn6Q2+oPWuLV3qBfbOj00kewlQ=,tag:Vr5iEQ2u+9YNahryhgzwSw==,type:str]", + "sops": { + "age": [ + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArTGJnaVhleUtsWnlITE1s\nNmRCaW1QWTFNSG5LRFdmb1lzRlV5NWl3YTBNCmF6eUN6RkJBZzc0MmJJa0dKTW01\ncVh4K1VLR2lURUpKQXpxNGpQNnpSUlUKLS0tIFJONnJFWkNCR3pqalRsUW9POVBj\nQ0pFQ3ltKzBETTVXTW5sV1ppWTFJc1kKzxUboNZO+Nwn2eTWy11VP9w1pRswCHaJ\nE2dYU0oUOClVzc0oSuIJxraG6TPj1N4WGC24gS+UmpkmSuCiOeZBsw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDSmxNR1I3dUFKT0xUVG9h\nWkRIakNVeWRQOEN3blNRVjZlWHF4K2NRa0hBClRiOXlmTTJ4T2JTUEw2c1l0R2N2\nMnRwdDA5bEZlQWJRTm9vUmNKclBSU1EKLS0tIFcyeDFjbTZyVEVDUjN1VzU1VHly\nNWNDMW9rTXY2bHNWYVR0SmtMckovUzQKhTWr6yFVW9am3okCiIswwqR5+/p9OLmB\nWCgPtwoFaBt1RjUXPK4/eS4LlucR2K6V/mNMn4xVsnkIl193U9632g==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age16j42pdc5dr6wnj7xayhkqdj2rny9u68fcqejs50hqq42scssh4gsnrrnlt" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2ME9CbmVGcENRWksyL0pW\nOEJyUGhpbGMwWlBhVXBSeXQ2MW1EWnFuR0E4CnMwU0pjdk1YMzF5ZEhTVFlBaHZq\nam94UWVGbjhZSEx6VHBmem9JRWgwYzAKLS0tIHFJZWRyRjRHNzhXdDJSYWN3bDlR\nYVp3eGJWWkh3Y09ZWElyclZQN1ZSVFkKjR32//EcFAdMjVlNgky5zvVkwXwEN68D\nrkTuHKjiO5aV7yAQGPkdNw0UM0oRGF0u4YF3oOUcZfSvnKgDeoi2Zw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1ug787sgt6st6k82fgkrug2lzltw4qsukrrqqs3w27ewwqj8rg4hsxcmylz" + } + ], + "lastmodified": "2026-07-28T00:27:46Z", + "mac": "ENC[AES256_GCM,data:AbJIHYcFpeanQsJ3x7RPL9Yjlg5BJgkepKax0fL9L/PpA03Antab93iUNG95Mp6k/duovp8Jm445lbuppDZq1dh9ij/deBa8GbzJ50wwEe9zMc3EwRKScpqZEhRPF7KlJsIjsHJyd8NkcI5ji49XkHb4Ae1//8zG5HpVgy+3b04=,iv:uxQCbMwMIfP5S1dbsvIx3F79YWEguxwox8T0YZvUBdc=,tag:3utjmBGDmPc8q4JjaXvCkA==,type:str]", + "version": "3.13.2" + } +}