From f5d29be04118779b17631b8f65d156a96a29d1fa Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 28 Jul 2026 10:24:23 +1000 Subject: [PATCH] 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 -- 2.54.0