From 5487490b8e9a6ba2877b4c4ae6363c067047af1b Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 09:42:38 +1000 Subject: [PATCH 1/8] feat(ipa): add AuthorizedKeysCommand + enroll tailscale-router - modules/ipa/client.nix: add AuthorizedKeysCommand so sshd fetches SSH public keys from IPA via sss_ssh_authorizedkeys, enabling pubkey login without per-host authorized_keys files - hosts/tailscale-router/host.nix: add IPA client module + networking.domain so SSSD runs and wayne can authenticate on this host - secrets/tailscale-router.keytab: sops-encrypted keytab for tailscale-router.sweet.home (generated by create-nixos-ipa-host-account.sh) - .sops.yaml: creation rule for secrets/tailscale-router.keytab Co-Authored-By: Claude Sonnet 4.6 --- .sops.yaml | 10 ++++++++++ hosts/tailscale-router/host.nix | 5 +++++ modules/ipa/client.nix | 8 ++++++++ secrets/tailscale-router.keytab | 26 ++++++++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 secrets/tailscale-router.keytab diff --git a/.sops.yaml b/.sops.yaml index 462abf5..c044d9c 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -127,6 +127,16 @@ 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: diff --git a/hosts/tailscale-router/host.nix b/hosts/tailscale-router/host.nix index 6198bf4..c95440c 100644 --- a/hosts/tailscale-router/host.nix +++ b/hosts/tailscale-router/host.nix @@ -6,10 +6,15 @@ 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/ipa/client.nix b/modules/ipa/client.nix index db83042..1aa8deb 100644 --- a/modules/ipa/client.nix +++ b/modules/ipa/client.nix @@ -60,6 +60,14 @@ in cacheCredentials = true; }; + # Fetch SSH public keys from IPA so users can log in with the key stored + # in their IPA profile rather than needing ~/.ssh/authorized_keys on every + # host. sss_ssh_authorizedkeys queries SSSD (which queries IPA LDAP). + services.openssh.extraConfig = '' + AuthorizedKeysCommand ${pkgs.sssd}/bin/sss_ssh_authorizedkeys %u + AuthorizedKeysCommandUser nobody + ''; + # 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. diff --git a/secrets/tailscale-router.keytab b/secrets/tailscale-router.keytab new file mode 100644 index 0000000..9f8c5d0 --- /dev/null +++ b/secrets/tailscale-router.keytab @@ -0,0 +1,26 @@ +{ + "data": "ENC[AES256_GCM,data:+/1AAVha+86abQIX8tebqPJ6BIcCCiTJgPe0OfFOwA2Mcx0NJdiPtQgyeo3G8fAolIQOFkk5reL+GUhlOz4iEbPOCWwFCckdH0tXiCiVcD35qQNMRurY8HmpM5FWFnyzBkKuyNZ8okPuo2+fA3NQh3gs94rpm2kBsfS18xvrlv9b8u1JvAxlH6GRMN1uUgFGmOXlpGAVjDUFiKUHN8tRSZpsxG0aKKPBZ82JdKgYfCiCQifS1366gIFrsjGriUC+P8wkadRnD1JE2ZAGSL7wJLaRmzA4LAMGXFGbitOsFxKxX3q8VWEXaaL+9h4rTe0WCrvmgDM9NUeVHpfRiJrLvQgrFZMUxuZF66vQVsLybjIWpYYJcjC6nfR0U3lMa9gjzOTMPxm8HdGKC53FhLM1HiPoe1a2Nno283fV0uaByMPmo257O6l1CXqb6KoZOGFccm7fKR/VnFAS1AInF+szI7/r+UcaTW6LwYeV3OwLE4a8b4OiqTLgCu6v,iv:kUlVXF4Yl5HGvoLwu9loiuuDVtz0kARRiUU3K+BPL2A=,tag:pVaGzQAX9Etoqc5XMGmNeg==,type:str]", + "sops": { + "age": [ + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6OUVUdkoxM1M5THlYZ1ky\nYWJ5RjhtRGoxTldwRkI2T1p6dHR4Y3gwR1ZrCjRDS1hTM01lZ0FSalBYQ1B0bENs\nUytXNmlrUkV4TmJkMUM1QW1ZaExIcGMKLS0tIHdHKzdEcUxvM0ZYUEp0a2d6SHp5\nYXQydy9uNG15V2FhUUd4NEFTMTNNMEUKkIzKEoYzoVs+nhnpkHFgDkQqrWykatND\ntsNxcr1SXSKeEW1m/QpXZnn/aW3zSQR09PqOHf7PYU47/AdkwrUaAg==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1njap586hc0q43kr03g6c8eqhdsmk8zcafkl3f83xwlc2gqhlmfgs4tmwad" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByZENrck42dWJjR2hYUVBl\ncERtRjR6dTdING5nWXlBc3o3eXFhaSs5NkFRCk43QXFUeTBJR0U5dG9YTDRmQ0Yx\naEo2blUvUXNZY3dpbEZRTUx1elNzdkkKLS0tIHJwNjRNM3V6WktWZ1BlUUtLRTNI\nMTVEbCtYbllIbTdxTGozWUluS3pIRXMKZCruDIkD/JofdAHWgPuaaKTDsz408ZkY\n77mhO8J+kd03qwt6qhFC5KF1lyjhwEnqrOE195+R/8Yl7hA/DsL2ZQ==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1k7d2du5mejsmv5rzavm4xwgpthqvcfsehduquv28nzs53zppa3kqngfxq2" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxaC95RjYybzVlU2tYdHdU\naGJ4M2IvNkJwVlNIU0VCZEFUZk5VaksrVnpFCmJXS3luTG9WejR0Rk9DbHA3dngy\nRElFWjU1N2xuc3JaKzQ0L0U2Yktib1kKLS0tIFlrK3F0ZFh5Ync3ejVWMzRKTUx2\nZDhxdEMwa3B4TFZUcWdTdktDeGt1QUEKfgYnK2lW3cZuJGaw+bAKDipLuC4S5vK2\nxK2eJB5TP/xXrp0F3lx9sc2b1FOY9Vt9IQ7zVlBqJFkzJrcw8zYJJw==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1zhfyuzlq40reuqlr34gf77852nhs3t6mqfzrqmas8z6sxk7tcfhsungrm0" + }, + { + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwOGFkSXNxTUIzLzR1Skk5\nMGZveHFDN3YwVmpwT3VTYkppWEU1aGVpdkhRCjZtRC92bVphd0ZFbzFxVzRUcGk4\nMUd1aWdEbFRaM01FT25JSVRoMTRsNUUKLS0tIDAybmcrYjFVWkFYaGY5TFZjcjFO\nYlZoQjJjN2lFNzd4RlJuSHFlRlNCOFEKgUIPnL2/OJgz9oMYt86/llHa7adTkhs6\n8yGYGV1wtU9aYtUMIR907SfYyZ6M4z8jH2wwzpQLbwQQMLgkejl1jg==\n-----END AGE ENCRYPTED FILE-----\n", + "recipient": "age1f7usptjx9rv4rxauasve200gxtdt9jkqhhdqstlf20wvlm7u75rsjfw50m" + } + ], + "lastmodified": "2026-07-27T23:14:51Z", + "mac": "ENC[AES256_GCM,data:CYhzR0Y29GXvWUBbb13s16v9hDQ4k4Xmd4FUpcyNtHe0L/IrDGkx1WHzAohNGZjGMNB9W3BgiEH85OoOo8r1F82El4/8s2prEJi8AARvQU3+2cmRjjhNCCYQWmJkoF/cZNpw6nVyWzZdfUpPvHjbuf6utlI3rtR0qiCpSo/32S0=,iv:XLKTU2Ute4jMkfRAbXiVGxqP9+fjSs6HwRv1JfTTe7w=,tag:oIqS/1HWQZ7mLWaO7GwLFA==,type:str]", + "version": "3.13.2" + } +} From 97019205da071301f9891c3177a7657844bb0fd9 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 09:47:11 +1000 Subject: [PATCH 2/8] fix(ipa): create home dir on first login + AuthorizedKeysCommand - security.pam.services.sshd.makeHomeDir: IPA users have no pre-created home directory on the host; without this, sshd opens a session to a missing directory and resets the connection immediately after auth - AuthorizedKeysCommand was already added in previous commit Co-Authored-By: Claude Sonnet 4.6 --- modules/ipa/client.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/ipa/client.nix b/modules/ipa/client.nix index 1aa8deb..2278fe7 100644 --- a/modules/ipa/client.nix +++ b/modules/ipa/client.nix @@ -68,6 +68,11 @@ in AuthorizedKeysCommandUser nobody ''; + # Create the home directory on first login if it doesn't exist yet. + # IPA users have no pre-created home on the host; without this sshd + # opens a session to a non-existent directory and resets the connection. + security.pam.services.sshd.makeHomeDir = 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. From a8d95aad02c3e6596b368eaf1584e75d6900165c Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 09:49:14 +1000 Subject: [PATCH 3/8] enable docker --- modules/build-types/gui.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/build-types/gui.nix b/modules/build-types/gui.nix index 093c325..0e2ee8f 100644 --- a/modules/build-types/gui.nix +++ b/modules/build-types/gui.nix @@ -1,6 +1,17 @@ { config, pkgs, lib, inputs, vars, ... }: { + nixpkgs.overlays = [ + (final: prev: { + docker = prev.docker_29; + docker_cli = prev.docker_29; + }) + ]; + + imports = [ + ../docker/enable-service.nix + ]; + environment.systemPackages = with pkgs; [ inputs.nixos-conf-editor.packages.${pkgs.stdenv.hostPlatform.system}.nixos-conf-editor nodejs From f3e5ea67a0b517ab07c0b980f8502ba99e13ac59 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 09:59:48 +1000 Subject: [PATCH 4/8] add docker group to user in enable docker service module --- modules/docker/enable-service.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/docker/enable-service.nix b/modules/docker/enable-service.nix index c5c23ba..b233c6e 100644 --- a/modules/docker/enable-service.nix +++ b/modules/docker/enable-service.nix @@ -15,7 +15,7 @@ # experimental = true; # }; }; - + users.users.${vars.primaryUser}.extraGroups = [ "docker" ]; environment.systemPackages = with pkgs; [ docker-compose docker-buildx From fc277294f3d1121cda771550065b5c8efeea55dd Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 10:02:58 +1000 Subject: [PATCH 5/8] add vars to module --- modules/docker/enable-service.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/docker/enable-service.nix b/modules/docker/enable-service.nix index b233c6e..99e1382 100644 --- a/modules/docker/enable-service.nix +++ b/modules/docker/enable-service.nix @@ -1,4 +1,4 @@ -{ pkgs, ... }: +{ pkgs, vars, ... }: { # virtualisation.docker.enable = true; From f46ae186721083a48ad21be9cfa133e828c2fd2d Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 10:08:57 +1000 Subject: [PATCH 6/8] fix(ipa): work around OpenSSH 10 AuthorizedKeysCommand path check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenSSH 10.0 tightened AuthorizedKeysCommand security by checking every path component of the command binary for group/world-write permission. /nix/store is 1775 (group-writable by nixbld), so sshd silently skips the command for any binary in the Nix store — causing IPA pubkey auth to silently fail with no diagnostic. Fix: copy sss_ssh_authorizedkeys to /usr/local/bin via systemd tmpfiles (C+ copies the file rather than symlinking, so the path at runtime is root-owned/755 throughout), and point AuthorizedKeysCommand at the copy. Co-Authored-By: Claude Sonnet 4.6 --- modules/ipa/client.nix | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/ipa/client.nix b/modules/ipa/client.nix index 2278fe7..3e543f2 100644 --- a/modules/ipa/client.nix +++ b/modules/ipa/client.nix @@ -63,8 +63,19 @@ in # Fetch SSH public keys from IPA so users can log in with the key stored # in their IPA profile rather than needing ~/.ssh/authorized_keys on every # host. sss_ssh_authorizedkeys queries SSSD (which queries IPA LDAP). + # + # /nix/store is 1775 (group-writable by nixbld). OpenSSH 10.0+ rejects + # AuthorizedKeysCommand binaries whose path contains any group-writable + # component, silently skipping the command. Copy to /usr/local/bin (all + # components root-owned, 755) so the path passes sshd's safety check. + systemd.tmpfiles.rules = [ + "d /usr/local 0755 root root - -" + "d /usr/local/bin 0755 root root - -" + "C+ /usr/local/bin/sss_ssh_authorizedkeys 0555 root root - ${pkgs.sssd}/bin/sss_ssh_authorizedkeys" + ]; + services.openssh.extraConfig = '' - AuthorizedKeysCommand ${pkgs.sssd}/bin/sss_ssh_authorizedkeys %u + AuthorizedKeysCommand /usr/local/bin/sss_ssh_authorizedkeys %u AuthorizedKeysCommandUser nobody ''; From 578ef70aa90b35b396db3965e9d202e813a19bdd Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 10:23:23 +1000 Subject: [PATCH 7/8] updated flake.lock --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index d5f6616..447467f 100644 --- a/flake.lock +++ b/flake.lock @@ -173,11 +173,11 @@ ] }, "locked": { - "lastModified": 1784350909, - "narHash": "sha256-ZWyzLbS1yKUTeFJLmdVuWNnHttL333/ldJbEE+KzCrM=", + "lastModified": 1785119570, + "narHash": "sha256-Rgs2xKnGLFWQscxUaXX07oyZeuMDOHEbqDOsgliLFGM=", "owner": "nix-community", "repo": "home-manager", - "rev": "4ce190229c73d44536caa7072f6308fb2d8feeb3", + "rev": "d4fd24667c8cbef124bb70a20380cab75ec8474d", "type": "github" }, "original": { @@ -259,11 +259,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1784432872, - "narHash": "sha256-n3gKTBIV4ZA5VQpUakffBe3KGu4+mhPoA34rrqS0GkA=", + "lastModified": 1785104993, + "narHash": "sha256-eKbrvPoAOFutbYMdbB3r5EQVmFxKv24iKqHPPUXA0gM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fd1462031fdee08f65fd0b4c6b64e22239a77870", + "rev": "8623c4c20aa4ca2f5fb81510d2944066c3fb0d96", "type": "github" }, "original": { From f5d29be04118779b17631b8f65d156a96a29d1fa Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 10:24:23 +1000 Subject: [PATCH 8/8] fix(ipa): harden script and update module docs Script fixes: - Rename HOSTNAME variable to TARGET (shadowed the bash builtin) - Fix ipa-getkeytab -s to always use IPA_SERVER, not DC_HOST (diverge if --dc is overridden to a jump host) - Remove dead REALM variable - Add EXIT trap to delete the plaintext keytab if the script aborts before sops encryption completes; cleared after successful encrypt - Distinguish real ipa host-add failures from "already exists" instead of swallowing all errors with || true - Warn explicitly when no platform age keys exist for the target (keytab would be admin-only and the host couldn't decrypt it at boot) - Fix sops fallback from pinned nixos-25.11 channel to nixpkgs (uses the repo's own flake.lock) - Expand "next steps" output to include networking.domain and nameservers lines that host.nix requires for IPA membership Module docs: - Point to the script as the primary setup path; move manual steps to a fallback section - Note that certs/ipa-ca.crt is already committed (no need to re-fetch) - Document the networking.domain and nameservers requirements in the header - Add sync-host-keys.sh as explicit step 0 Co-Authored-By: Claude Sonnet 4.6 --- modules/ipa/client.nix | 44 ++++---- scripts/ipa/create-nixos-ipa-host-account.sh | 109 +++++++++++++------ 2 files changed, 97 insertions(+), 56 deletions(-) diff --git a/modules/ipa/client.nix b/modules/ipa/client.nix index 3e543f2..2cec678 100644 --- a/modules/ipa/client.nix +++ b/modules/ipa/client.nix @@ -6,35 +6,39 @@ # # Usage (in a host.nix imports list): # (import ../../modules/ipa/client.nix { -# keytabSopsFile = ../../secrets/nix-cache.keytab; -# caCertFile = ../../certs/ipa-ca.crt; +# 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): # -# 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. +# 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. # -# 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 +# 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. # -# 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): +# 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. +# +# Manual fallback (if the script isn't usable): +# a. On the FreeIPA server: ipa host-add [--ip-address=] --force +# b. On the FreeIPA server: ipa-getkeytab -s -p host/ -k /tmp/.keytab +# c. From the repo root (path must match for sops creation rule to apply): # 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 -# sops updatekeys secrets/.keytab -# Commit the encrypted file. +# d. Commit secrets/.keytab and the updated .sops.yaml, then deploy. # -# 4. nixos-rebuild switch (or create-proxmox-resource.sh) — no further -# manual enrollment steps required. -# -# vars dependencies: homeDomain, ipaServer +# vars dependencies: homeDomain, ipaServer, domainControllerIp { keytabSopsFile, caCertFile }: { config, lib, pkgs, vars, ... }: diff --git a/scripts/ipa/create-nixos-ipa-host-account.sh b/scripts/ipa/create-nixos-ipa-host-account.sh index a941912..a93b507 100755 --- a/scripts/ipa/create-nixos-ipa-host-account.sh +++ b/scripts/ipa/create-nixos-ipa-host-account.sh @@ -17,7 +17,7 @@ # # Options: # --ip Register this IP with the IPA host record (optional). -# --dc SSH to this host for ipa-getkeytab. +# --dc SSH to this host to run IPA commands. # Default: $IPA_SERVER (from env.sh / environment). # --dc-user SSH user on the domain controller. Default: wayne. # --dry-run Print what would be done without making any changes. @@ -35,7 +35,9 @@ # scripts/secrets/sync-host-keys.sh first so the host # can decrypt its own keytab on boot. This script adds the .sops.yaml # creation rule for secrets/.keytab automatically, but the -# host age key anchor (&lxc- etc.) must already exist. +# host age key anchor (&lxc- etc.) must already exist — +# otherwise only the admin key can decrypt the keytab and the deployed +# host will fail to read it. # 4. sops in PATH, or Nix available to run it via `nix run`. set -euo pipefail @@ -52,7 +54,7 @@ DC_HOST="${IPA_SERVER}" DC_USER="wayne" IP_ADDR="" DRY_RUN=false -HOSTNAME="" +TARGET="" usage() { sed -n '/^# Usage:/,/^[^#]/{ /^#/{ s/^# \?//; p } }' "$0" @@ -68,22 +70,21 @@ while [[ $# -gt 0 ]]; do -h|--help) usage 0 ;; -*) echo "Unknown flag: $1" >&2; usage 1 ;; *) - if [[ -n "${HOSTNAME}" ]]; then echo "Unexpected argument: $1" >&2; usage 1; fi - HOSTNAME="$1"; shift + if [[ -n "${TARGET}" ]]; then echo "Unexpected argument: $1" >&2; usage 1; fi + TARGET="$1"; shift ;; esac done -if [[ -z "${HOSTNAME}" ]]; then +if [[ -z "${TARGET}" ]]; then echo "Error: hostname required." >&2 usage 1 fi -FQDN="${HOSTNAME}.${HOME_DOMAIN}" -REALM="${HOME_DOMAIN^^}" # uppercase: SWEET.HOME -KEYTAB_SECRET="${REPO_ROOT}/secrets/${HOSTNAME}.keytab" +FQDN="${TARGET}.${HOME_DOMAIN}" +KEYTAB_SECRET="${REPO_ROOT}/secrets/${TARGET}.keytab" # Temp path on the domain controller — use a name that won't collide. -DC_TMP="/tmp/nixos-keytab-${HOSTNAME}-$$.keytab" +DC_TMP="/tmp/nixos-keytab-${TARGET}-$$.keytab" # --- Helpers --- @@ -112,8 +113,8 @@ dc_run() { if command -v sops &>/dev/null; then SOPS_CMD=(sops) else - log "sops not in PATH — will use 'nix run github:NixOS/nixpkgs/nixos-25.11#sops'" - SOPS_CMD=(nix run "github:NixOS/nixpkgs/nixos-25.11#sops" --) + log "sops not in PATH — will use 'nix run nixpkgs#sops'" + SOPS_CMD=(nix run "nixpkgs#sops" --) fi # --- Preflight checks --- @@ -131,13 +132,13 @@ cd "${REPO_ROOT}" # also exist at that point or sops will refuse with "no matching creation # rules found." -log "Checking .sops.yaml for creation rule: secrets/${HOSTNAME}.keytab" +log "Checking .sops.yaml for creation rule: secrets/${TARGET}.keytab" RULE_EXISTS=false # Match "path_regex: secrets/...keytab" — using .*keytab rather # than \.keytab because the file stores the regex verbatim (\.keytab = two # chars: backslash + dot), which a BRE \. (= escaped literal dot) won't span. -if grep -q "path_regex: secrets/${HOSTNAME}.*keytab" .sops.yaml 2>/dev/null; then +if grep -q "path_regex: secrets/${TARGET}.*keytab" .sops.yaml 2>/dev/null; then RULE_EXISTS=true logn "Rule already exists — skipping addition." fi @@ -149,12 +150,20 @@ if ! $RULE_EXISTS; then # that have been registered get added as recipients. RECIPIENTS=("*admin") for platform in lxc proxmox linode; do - anchor="${platform}-${HOSTNAME}" + anchor="${platform}-${TARGET}" if grep -q "^ - &${anchor} " .sops.yaml; then RECIPIENTS+=("*${anchor}") fi done + if [[ ${#RECIPIENTS[@]} -eq 1 ]]; then + echo "Warning: no platform age keys found for '${TARGET}' in .sops.yaml." >&2 + echo " Run scripts/secrets/sync-host-keys.sh first," >&2 + echo " otherwise only the admin key can decrypt the keytab and the" >&2 + echo " deployed host won't be able to read it at boot." >&2 + echo " Continuing with admin-only encryption..." >&2 + fi + # Build the indented recipient list for the YAML block. RECIPIENT_YAML="" for r in "${RECIPIENTS[@]}"; do @@ -163,9 +172,9 @@ if ! $RULE_EXISTS; then RECIPIENT_YAML="${RECIPIENT_YAML%$'\n'}" # strip trailing newline NEW_RULE=" - # Host keytab for ${HOSTNAME} FreeIPA enrollment (binary sops file). + # Host keytab for ${TARGET} FreeIPA enrollment (binary sops file). # Generated by scripts/ipa/create-nixos-ipa-host-account.sh. - - path_regex: secrets/${HOSTNAME}\\.keytab\$ + - path_regex: secrets/${TARGET}\\.keytab\$ key_groups: - age: ${RECIPIENT_YAML}" @@ -206,23 +215,37 @@ IP_FLAG="" [[ -n "${IP_ADDR}" ]] && IP_FLAG="--ip-address=${IP_ADDR}" # --force: create the host record even if DNS doesn't resolve it yet. -# Pipe through grep to suppress the "already exists" warning without -# hiding real errors (ipa exits 1 for real errors, 0 for already-exists). -HOST_ADD_CMD="sudo ipa host-add '${FQDN}' ${IP_FLAG} --force 2>&1 | \ - tee /dev/stderr | grep -q 'already exists' && echo '(host already registered)' || true" -dc_run "bash -c \"${HOST_ADD_CMD}\"" +if $DRY_RUN; then + echo "[dry-run] ssh ${DC_USER}@${DC_HOST} sudo ipa host-add '${FQDN}' ${IP_FLAG} --force" +else + HOST_ADD_OUT=$(ssh "${DC_USER}@${DC_HOST}" "sudo ipa host-add '${FQDN}' ${IP_FLAG} --force 2>&1") \ + && HOST_ADD_RC=0 || HOST_ADD_RC=$? + if [[ $HOST_ADD_RC -eq 0 ]]; then + echo "${HOST_ADD_OUT}" + elif echo "${HOST_ADD_OUT}" | grep -q "already exists"; then + logn "(host already registered)" + else + echo "Error: ipa host-add failed (exit ${HOST_ADD_RC}):" >&2 + echo "${HOST_ADD_OUT}" >&2 + exit 1 + fi +fi # --- Step 3: Fetch the keytab from the domain controller --- log "Fetching keytab for host/${FQDN}" -dc_run "sudo ipa-getkeytab -s '${DC_HOST}' -p 'host/${FQDN}' -k '${DC_TMP}'" +# Remove the plaintext keytab if the script aborts before encryption completes. +# The trap is cleared at the end of step 4 once sops has encrypted it in-place. +trap 'rm -f "${KEYTAB_SECRET}"' EXIT + +dc_run "sudo ipa-getkeytab -s '${IPA_SERVER}' -p 'host/${FQDN}' -k '${DC_TMP}'" if $DRY_RUN; then - echo "[dry-run] Would stream ${DC_USER}@${DC_HOST}:${DC_TMP} → secrets/${HOSTNAME}.keytab" + echo "[dry-run] Would stream ${DC_USER}@${DC_HOST}:${DC_TMP} → secrets/${TARGET}.keytab" else - logn "Streaming keytab from ${DC_HOST}:${DC_TMP} → secrets/${HOSTNAME}.keytab" - # scp can't read a root-owned temp file as wayne; pipe through sudo cat instead. + logn "Streaming keytab from ${DC_HOST}:${DC_TMP} → secrets/${TARGET}.keytab" + # scp can't read a root-owned temp file as ${DC_USER}; pipe through sudo cat instead. ssh "${DC_USER}@${DC_HOST}" "sudo cat '${DC_TMP}'" > "${KEYTAB_SECRET}" logn "Removing temp file on ${DC_HOST}" @@ -235,24 +258,38 @@ fi # sops matches the creation rule by path. Using -i (in-place) rather than # stdout redirect keeps the path intact through the encrypt call. -log "Encrypting secrets/${HOSTNAME}.keytab in-place with sops" +log "Encrypting secrets/${TARGET}.keytab in-place with sops" run "${SOPS_CMD[@]}" -e --input-type binary -i "${KEYTAB_SECRET}" +# Encryption succeeded — the file is now sops-encrypted; cancel the cleanup trap. +trap - EXIT + # --- Done --- if ! $DRY_RUN; then echo "" - echo "Done. secrets/${HOSTNAME}.keytab is sops-encrypted and ready." + echo "Done. secrets/${TARGET}.keytab is sops-encrypted and ready." echo "" echo "Next steps:" - echo " 1. Verify: grep '\"data\": \"ENC' secrets/${HOSTNAME}.keytab" + echo " 1. Verify: grep '\"data\": \"ENC' secrets/${TARGET}.keytab" echo " 2. Stage and commit:" - echo " git add secrets/${HOSTNAME}.keytab .sops.yaml" - echo " git commit -m 'secrets: add IPA keytab for ${HOSTNAME}'" - echo " 3. Add the module to hosts/${HOSTNAME}/host.nix:" - echo " (import ../../modules/ipa/client.nix {" - echo " keytabSopsFile = ../../secrets/${HOSTNAME}.keytab;" - echo " caCertFile = ../../certs/ipa-ca.crt;" - echo " })" + echo " git add secrets/${TARGET}.keytab .sops.yaml" + echo " git commit -m 'secrets: add IPA keytab for ${TARGET}'" + echo " 3. Add to hosts/${TARGET}/host.nix (networking block and imports):" + echo "" + echo " networking = {" + echo " hostName = \"${TARGET}\";" + echo " domain = vars.homeDomain; # required for Kerberos FQDN" + echo " nameservers = [ vars.domainControllerIp ]; # IPA DNS" + echo " ..." + echo " };" + echo "" + echo " imports = [" + echo " (import ../../modules/ipa/client.nix {" + echo " keytabSopsFile = ../../secrets/${TARGET}.keytab;" + echo " caCertFile = ../../certs/ipa-ca.crt;" + echo " })" + echo " ];" + echo "" echo " 4. Deploy: nixos-rebuild switch (or create-proxmox-resource.sh)" fi