From 1680dd9de56adbe7c639cbf286f81b5230ddf72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edward=20Tj=C3=B6rnhammar?= Date: Sat, 14 Mar 2026 17:20:36 +0100 Subject: [PATCH] nixosTests.slurm-pam: add PAM integration testing for Slurm --- nixos/tests/all-tests.nix | 1 + nixos/tests/slurm-pam.nix | 411 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+) create mode 100644 nixos/tests/slurm-pam.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 672331ab645d..fde1852208be 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1492,6 +1492,7 @@ in slimserver = runTest ./slimserver.nix; slipshow = runTest ./slipshow.nix; slurm = runTest ./slurm.nix; + slurm-pam = runTest ./slurm-pam.nix; smokeping = runTest ./smokeping.nix; snapcast = runTest ./snapcast.nix; snapper = runTest ./snapper.nix; diff --git a/nixos/tests/slurm-pam.nix b/nixos/tests/slurm-pam.nix new file mode 100644 index 000000000000..1bb613af38a0 --- /dev/null +++ b/nixos/tests/slurm-pam.nix @@ -0,0 +1,411 @@ +{ lib, pkgs, ... }: + +let + + slurmConf = "/etc/slurm.conf"; + inherit (import ./ssh-keys.nix pkgs) + snakeOilPrivateKey + snakeOilPublicKey + ; + sshConf = '' + UserKnownHostsFile /dev/null + StrictHostKeyChecking no + ''; + sshConfigPubkey = pkgs.writeText "ssh_config_pubkey" ( + sshConf + + '' + + BatchMode yes + PreferredAuthentications publickey + KbdInteractiveAuthentication no + PasswordAuthentication no + IdentityFile /root/privkey.snakeoil + '' + ); + sshConfigPassword = pkgs.writeText "ssh_config_password" ( + sshConf + + '' + BatchMode no + PreferredAuthentications password + PubkeyAuthentication no + KbdInteractiveAuthentication no + PasswordAuthentication yes + '' + ); + sshOpts = "-F " + sshConfigPubkey; + sshPassOpts = "-F " + sshConfigPassword; + adoptRemoteScript = pkgs.writeShellScript "slurm-pam-adopt-remote" '' + echo $$ > /home/submitter/ssh.pid + trap : TERM INT + while true; do + sleep 1 + done + ''; + mkWaitJob = + node: name: + pkgs.writeText "${name}.sbatch" '' + #!${pkgs.runtimeShell} + #SBATCH --job-name=${name} + #SBATCH --nodes=1 + #SBATCH --nodelist=${node} + while true; do sleep 60; done + ''; + + slurmconfig = + { config, ... }: + { + services.slurm = { + controlMachine = "control"; + nodeName = map (n: n + " CPUs=1 State=UNKNOWN") [ + "regular" + "pamslurm" + "pamslurmadopt" + ]; + partitionName = [ "debug Nodes=ALL Default=YES MaxTime=INFINITE State=UP" ]; + extraConfig = '' + PrologFlags=contain + ProctrackType=proctrack/cgroup + TaskPlugin=task/cgroup,task/affinity + SlurmdDebug=debug + ''; + }; + + services.openssh = { + enable = true; + settings = { + AllowUsers = [ "submitter" ]; + PubkeyAuthentication = true; + # leave password auth available on the regular node for + # regression testing plain pam_unix behavior + KbdInteractiveAuthentication = false; + PasswordAuthentication = true; + }; + }; + + environment.systemPackages = [ + pkgs.pamtester + pkgs.sshpass + ]; + + networking.firewall.enable = false; + systemd.tmpfiles.rules = [ + "f /etc/munge/munge.key 0400 munge munge - mungeverryweakkeybuteasytointegratoinatest" + ]; + environment.etc."slurm.conf".source = "${config.services.slurm.etcSlurm}/slurm.conf"; + systemd.services.sshd.environment.SLURM_CONF = slurmConf; + users.groups.submitter = { }; + users.users.submitter = { + isNormalUser = true; + createHome = true; + initialPassword = "submitter"; + group = "submitter"; + openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; + }; + }; + +in +{ + name = "slurm-pam"; + + meta.maintainers = [ lib.maintainers.edwtjo ]; + + nodes = + let + computeNode = + { ... }: + { + imports = [ slurmconfig ]; + services.slurm = { + client.enable = true; + }; + }; + computePAMNode = + { ... }: + { + imports = [ computeNode ]; + security.pam.services.sshd.slurm.enable = true; + services.openssh.settings = { + KbdInteractiveAuthentication = false; + PasswordAuthentication = lib.mkForce false; + PubkeyAuthentication = true; + }; + }; + computePAMAdoptNode = + { ... }: + { + imports = [ computeNode ]; + # NOTE: Prolog, Epilog needed for more advanced tests. + # This is an upstream recommended workaround for removing pam_systemd + services.slurm.extraConfig = '' + LaunchParameters=ulimit_pam_adopt + SrunProlog=${pkgs.writers.writeBash "slurm-prolog" '' + loginctl enable-linger $SLURM_JOB_USER + exit 0 + ''} + TaskProlog=${pkgs.writers.writeBash "slurm-taskprolog" '' + echo "export XDG_RUNTIME_DIR=/run/user/$SLURM_JOB_UID" + echo "export XDG_SESSION_ID=$(/tmp/wait-pamslurm.out 2>/tmp/wait-pamslurm.err &\"" + ) + submit.wait_until_succeeds( + "squeue -h -u submitter -o '%N %u %T %j' | " + "grep -Fx 'pamslurm submitter RUNNING wait-pamslurm'" + ) + submit.fail( + "ssh ${sshOpts} submitter@pamslurmadopt true", + timeout=30 + ) + submit.succeed("runuser -u submitter -- scancel -n wait-pamslurm") + submit.wait_until_succeeds( + "! squeue -h -u submitter -o '%j' | grep -Fx wait-pamslurm" + ) + + # Positive integration tests for `pam_slurm_adopt`. + # + # This subtest checks the following: + # + # 1. a job can be created specifically on `pamslurmadopt`; + # 2. Slurm reports that the job is running on that node; + # 3. the node has local Slurm state for that job (`scontrol listpids`); + # 4. SSH login is allowed while the job is active; + # 5. a long-lived SSH session is adopted into Slurm's job tracking; + # 6. cancelling the job tears down the adopted process; + # 7. SSH access is denied again once the job is gone. + # + # Validates both policy enforcement and process adoption/cleanup. + with subtest("pam_slurm_adopt_adopts_connection"): + pamslurmadopt_job = submit.succeed( + "runuser -u submitter -- sbatch --parsable ${mkWaitJob "pamslurmadopt" "wait-pamslurmadopt"}" + ).strip() + + submit.wait_until_succeeds( + f"test \"$(squeue -h -j {pamslurmadopt_job} -o %T)\" = RUNNING" + ) + submit.wait_until_succeeds( + f"squeue -h -j {pamslurmadopt_job} -o %N | grep -Fx pamslurmadopt" + ) + pamslurmadopt.wait_until_succeeds( + f"scontrol listpids | awk -v jid='{pamslurmadopt_job}' 'NR > 1 && $2 == jid {{ found = 1 }} END {{ exit !found }}'" + ) + + # Short SSH probe: verifies that login is allowed at all while the job + # is active, before we move on to the stronger adoption assertions. + submit.succeed( + "ssh ${sshOpts} submitter@pamslurmadopt true", + timeout=30 + ) + + # Start a persistent SSH session whose remote process records its PID. + # We later use that PID to prove the session was adopted into Slurm's + # accounting/control path and then cleaned up when the job is cancelled. + submit.succeed( + "sh -lc '" + "ssh ${sshOpts} submitter@pamslurmadopt ${adoptRemoteScript}" + ">/tmp/adopt-ssh.log 2>&1 & " + "echo -n \\$! > /tmp/adopt-ssh.clientpid'" + ) + + # Wait until the remote helper has started and published its PID. + pamslurmadopt.wait_until_succeeds("test -s /home/submitter/ssh.pid") + + # Prove that the SSH-spawned remote process is visible through Slurm's + # local process listing, i.e. that the session was adopted into the job. + pamslurmadopt.wait_until_succeeds( + "pid=$(cat /home/submitter/ssh.pid); " + "scontrol listpids | awk 'NR > 1 { print $1 }' | grep -Fx \"$pid\"" + ) + remote_pid = pamslurmadopt.succeed("cat /home/submitter/ssh.pid").strip() + + # Cancel the allocation and verify the entire chain is torn down: + # the Slurm job disappears, the adopted remote process exits, and the + # local SSH client exits as well. + submit.succeed(f"runuser -u submitter -- scancel {pamslurmadopt_job}") + submit.wait_until_succeeds( + f"test -z \"$(squeue -h -j {pamslurmadopt_job})\"", + timeout=60, + ) + pamslurmadopt.wait_until_succeeds( + f"! test -e /proc/{remote_pid}", + timeout=60, + ) + submit.wait_until_succeeds( + "! kill -0 $(cat /tmp/adopt-ssh.clientpid)", + timeout=60, + ) + + # Once the job is gone, SSH must be denied + # again on the adopt-protected node. + submit.fail( + "ssh ${sshOpts} submitter@pamslurmadopt true", + timeout=30 + ) + ''; +}