Archived
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m33s
Two independent fixes: 1. hashedPassword = "!" on the IPA user stub Without any shadow entry, pam_unix returns PAM_AUTHINFO_UNAVAIL before prompting, so PAM_AUTHTOK is never set. The "!" marker (account locked, not a real hash) ensures a shadow entry exists so pam_unix prompts and sets PAM_AUTHTOK — which the subsequent pam_sss module can then use. 2. pam_sss try_first_pass instead of use_first_pass (login + su) use_first_pass silently fails when PAM_AUTHTOK is unset. try_first_pass prompts independently in that case, making IPA password auth work even if pam_unix returns early for any reason. Root cause found during incident: wayne (uid 50002) was outside IPA's auto-SID range (163800000+), so sidgen never assigned him an ipaNTSecurityIdentifier. Without it, ipadb's handle_authdata step fails with "Generic error" after SPAKE pre-auth succeeds. Fixed by manually adding objectClass: ipaNTUserAttrs + ipaNTSecurityIdentifier RID 550002 to wayne's LDAP entry on domain-controller. Any future IPA user with a manually-assigned uid outside the auto-range needs the same treatment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
211 lines
9.5 KiB
Nix
211 lines
9.5 KiB
Nix
# Fully declarative FreeIPA domain membership.
|
|
#
|
|
# 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/<hostname>.keytab; is a no-op for all other hosts.
|
|
#
|
|
# To enroll a new host:
|
|
# 0. scripts/secrets/sync-host-keys.sh <flake-target>
|
|
# 1. scripts/ipa/create-nixos-ipa-host-account.sh [--ip <addr>] <hostname>
|
|
# (adds .sops.yaml rule, runs ipa host-add, encrypts keytab in one step)
|
|
# 2. git add secrets/<hostname>.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 <fqdn> [--ip-address=<ip>] --force
|
|
# b. On the FreeIPA server: ipa-getkeytab -s <ipa-server> -p host/<fqdn> -k /tmp/<host>.keytab
|
|
# c. From the repo root (path must match for sops creation rule to apply):
|
|
# cp /tmp/<host>.keytab secrets/<host>.keytab
|
|
# sops -e --input-type binary -i secrets/<host>.keytab
|
|
# d. Commit secrets/<host>.keytab and the updated .sops.yaml, then deploy.
|
|
#
|
|
# vars dependencies: homeDomain, ipaServer, domainControllerIp, ipaUser
|
|
|
|
{ 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 ../../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;
|
|
inherit realm;
|
|
server = vars.ipaServer;
|
|
certificate = caCertPkg;
|
|
inherit basedn;
|
|
ipaHostname = fqdn;
|
|
offlinePasswords = true;
|
|
cacheCredentials = true;
|
|
};
|
|
|
|
# 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.
|
|
# lightdm also needs this so the GUI login path can create the home dir
|
|
# if it was not pre-seeded by the tmpfiles rule above (e.g. on first boot
|
|
# before SSSD has resolved the user).
|
|
pam.services = {
|
|
sshd.makeHomeDir = true;
|
|
lightdm.makeHomeDir = true;
|
|
|
|
# pam_unix returns PAM_AUTHINFO_UNAVAIL without prompting when the local
|
|
# stub has "!" in shadow (account locked), so PAM_AUTHTOK is never set
|
|
# and pam_sss's use_first_pass fails with "No authentication token".
|
|
# Changing to try_first_pass makes pam_sss prompt independently when no
|
|
# prior module has set the token, restoring IPA password login via
|
|
# LightDM and su.
|
|
login.rules.auth.sss.settings = lib.mkForce { try_first_pass = true; };
|
|
su.rules.auth.sss.settings = lib.mkForce { try_first_pass = true; };
|
|
};
|
|
|
|
# HM with useUserPackages = true (flake.nix) sets users.users.${ipaUser}.packages,
|
|
# which forces the stub into /etc/passwd. pam_sss.so with the "localusers" flag
|
|
# (added by NixOS when SSSD is enabled) then skips SSSD for any user it finds in
|
|
# local /etc/passwd — including this stub — falling through to pam_unix, which has
|
|
# no password for the stub → sudo auth always fails.
|
|
#
|
|
# Fix: NOPASSWD for the IPA user. The IPA user already authenticated to reach a
|
|
# shell (SSH public key from IPA or Kerberos), so re-prompting via a broken PAM
|
|
# path is security theater on a single-admin homelab.
|
|
sudo.extraRules = [{
|
|
users = [ vars.ipaUser ];
|
|
commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
|
|
}];
|
|
};
|
|
|
|
systemd = {
|
|
# 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.
|
|
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"
|
|
# Pre-create the IPA user's home dir so Home Manager activation succeeds
|
|
# even before their first login. On a fresh system SSSD may not have
|
|
# resolved the user yet — tmpfiles warns and skips in that case (non-fatal),
|
|
# and pam_mkhomedir covers the first-login path as a fallback.
|
|
"d /home/${vars.ipaUser} 0700 ${vars.ipaUser} ${vars.ipaUser} - -"
|
|
];
|
|
|
|
# security.ipa enables Kerberos (security.krb5) which causes systemd to
|
|
# start auth-rpcgss-module.service and rpc-gssd.service for Kerberos NFS
|
|
# authentication. LXC containers can't load the auth_rpcgss kernel module
|
|
# and don't have /var/lib/nfs/rpc_pipefs, so both services fail.
|
|
#
|
|
# The NixOS IPA module already adds a drop-in for auth-rpcgss-module.service
|
|
# with ConditionPathExists=/etc/krb5.keytab. We use lib.mkForce to win the
|
|
# text conflict and add ConditionVirtualization=!container alongside it so
|
|
# the service is skipped (not failed) in containers that do have a keytab.
|
|
# Same fix for rpc-gssd.service which also fails in containers.
|
|
units = lib.mkIf config.boot.isContainer {
|
|
"auth-rpcgss-module.service" = {
|
|
overrideStrategy = "asDropinIfExists";
|
|
text = lib.mkForce ''
|
|
[Unit]
|
|
ConditionPathExists=
|
|
ConditionPathExists=/etc/krb5.keytab
|
|
ConditionVirtualization=!container
|
|
'';
|
|
};
|
|
# rpc-gssd also has ConditionPathExists from the NixOS IPA module (and an
|
|
# X-Restart-Triggers store path from systemd.nix). Use mkForce to win;
|
|
# omit X-Restart-Triggers since this service is skipped in containers anyway.
|
|
"rpc-gssd.service" = {
|
|
overrideStrategy = "asDropinIfExists";
|
|
text = lib.mkForce ''
|
|
[Unit]
|
|
ConditionPathExists=
|
|
ConditionPathExists=/etc/krb5.keytab
|
|
ConditionVirtualization=!container
|
|
'';
|
|
};
|
|
};
|
|
|
|
# home-manager-<user>.service fails on first enrollment because /home/wayne
|
|
# doesn't exist until the user's first login (pam_mkhomedir creates it then).
|
|
# ConditionPathExists makes systemd skip the service (exit 0, condition not
|
|
# met) instead of failing. After first login the dir exists and subsequent
|
|
# rebuilds activate HM normally.
|
|
services."home-manager-${vars.ipaUser}".unitConfig.ConditionPathExists =
|
|
"/home/${vars.ipaUser}";
|
|
};
|
|
|
|
services.openssh.extraConfig = ''
|
|
AuthorizedKeysCommand /usr/local/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.
|
|
sops.secrets."ipa-host-keytab" = {
|
|
sopsFile = keytabPath;
|
|
format = "binary";
|
|
path = "/etc/krb5.keytab";
|
|
owner = "root";
|
|
group = "root";
|
|
mode = "0600";
|
|
restartUnits = [ "sssd.service" ];
|
|
};
|
|
|
|
# NixOS requires isNormalUser/isSystemUser + group on any entry in
|
|
# users.users. HM with useUserPackages = true (set in flake.nix) adds a stub
|
|
# entry for each HM user so it can install packages to
|
|
# /etc/profiles/per-user/<name>/. This definition satisfies those assertions.
|
|
# With security.ipa setting "passwd: sss files" in nsswitch, SSSD's IPA entry
|
|
# takes priority for NSS lookups — this local stub is only a fallback when
|
|
# SSSD is unreachable (at which point auth fails anyway).
|
|
users.users.${vars.ipaUser} = {
|
|
isNormalUser = true;
|
|
group = "users";
|
|
extraGroups = [ "wheel" ];
|
|
createHome = false;
|
|
# "!" is not a password hash — it is the standard "account locked" marker.
|
|
# It cannot authenticate anyone locally. It exists solely so NixOS generates
|
|
# a shadow entry for this stub user; without one pam_unix returns
|
|
# PAM_AUTHINFO_UNAVAIL before prompting, which means PAM_AUTHTOK is never
|
|
# set and the subsequent pam_sss use_first_pass call has nothing to work
|
|
# with — blocking LightDM and su logins even when IPA/SSSD auth succeeds.
|
|
hashedPassword = "!";
|
|
};
|
|
|
|
# Home Manager config for the IPA primary user, applied on every enrolled
|
|
# host. Manages what IPA doesn't: dotfiles, user-scoped packages, session
|
|
# variables. Switch-nix/Test-nix/buildImage are system-wide (configuration.nix)
|
|
# so they don't need to be repeated here.
|
|
#
|
|
# homeDirectory uses mkForce because HM's NixOS integration module sets it to
|
|
# "/var/empty" for users not found in config.users.users at eval time (SSSD
|
|
# users aren't visible there).
|
|
home-manager.users.${vars.ipaUser} = { pkgs, ... }: {
|
|
home = {
|
|
username = vars.ipaUser;
|
|
homeDirectory = lib.mkForce "/home/${vars.ipaUser}";
|
|
stateVersion = "26.05";
|
|
packages = with pkgs; [ tmux sshfs ];
|
|
sessionVariables.EDITOR = "nano";
|
|
};
|
|
programs.home-manager.enable = true;
|
|
programs.bash.enable = true;
|
|
};
|
|
}
|