Written to /workspace/solution.md (360 lines, self-contained runbook).
Root cause — version skew, not a user error
The host has the SSH-side hardening but the deployed bunkerd predates the spawn-side half. Trace through the installed block:
session [success=2 auth_err=ignore default=die] pam_succeed_if.so quiet user !~ bunker-*
session [success=ignore default=die] pam_exec.so quiet /usr/lib/bunker/pam-tmp-guard verify bunker-agents
session required pam_namespace.so
For a bunker-* user, pam_succeed_if is false → pam_exec runs the fail-closed helper → helper exits 1 (user <u> is not a member of bunker-agents) → pam_exec returns PAM_SYSTEM_ERR → default=die denies the session. sshd reports the generic pam_open_session(): System error; sshfs' SFTP channel is torn down mid-handshake. Non-pattern users are jumped over by success=2, which is why only agents break.
The old daemon spawned agents without running the isolation-provision group stage, so every agent it created was outside bunker-agents.
One-command triage (before touching PAM)
PAM_USER=<agent> /usr/lib/bunker/pam-tmp-guard verify bunker-agents; echo rc=$?
# rc=1 + "not a member" => this bug
Exact fix
bunkerd whose spawn path adds the group before ready, restart it, then back-fill existing agents (no agent restart needed — group membership resolves at each session open):
sh
getent passwd | awk -F: '$1 ~ /^bunker-/ {print $1}' | while read -r u; do
id -nG "$u" | tr ' ' '\n' | grep -qx bunker-agents || usermod -aG bunker-agents "$u"
donepam_exec precondition fails closed again)./usr/share/bunker/hardening.min-daemon-version + strings bunkerd | grep isolation-provision-group) that refuses a too-old daemon; and spawn must usermod -aG bunker-agents + verify before reporting ready.Verification — re-run the helper (expect rc=0 for all agents), then ssh -o BatchMode=yes <agent>@host 'echo SSH_OK; id -nG' (expect rc=0, not 254), plus scp/sshfs and a clean journalctl -u ssh | grep pam_open_session. The document also preserves the fail-closed behavior for genuine non-members and documents the refuted allow_other//etc/fuse.conf and MOTD red herrings.
# Evidence - Problem class: pam-exec-precondition-dies-ssh-session-for-group-members - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T16:00:28.280Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: every SSH session for users matching the reserved pattern dies immediately after successful public-key auth. sshd journal: 'pam_unix(sshd:session): session opened for user bunker-<agent>(uid=1010)' followed by 'error: PAM: pam_open_session(): System error'. The client sees NO command output and exit status 254; sshfs reports 'remote host has disconnected' / 'read: Connection reset by peer'. So `exec`-over-ssh, `mount` (sshfs) and `cp` all fail against a HEALTHY, running agent while the daemon reports it as running.\n\nMECHANISM: the installed PAM block is\n session [success=2 auth_err=ignore default=die] pam_succeed_if.so quiet user !~ bunker-*\n session [success=ignore default=die] pam_exec.so quiet /usr/lib/bunker/pam-tmp-guard verify bunker-agents\n session required pam_namespace.so\nThe helper fails closed on membership: run with a user who is NOT in the required group it prints 'bunker-pam-guard: user <u> is not a member of bunker-agents' and exits 1. pam_exec maps a non-zero exit to PAM_SYSTEM_ERR, and the control field `default=die` turns that into a denied session - reported by sshd as the generic 'System error'. Because sshfs uses the SFTP subsystem over the same session, its channel is torn down mid-handshake.\n\nWHY IT HAPPENED (version skew, not a user error): the SSH-side half of the hardening was installed on the host by the provisioning command, but the DEPLOYED daemon binary predated the spawn-side half that adds each new agent to the required group (the 'isolation-provision' spawn stage). Host provisioned + old daemon = every agent that daemon spawns is locked out of SSH.\n\nFIX / DIAGNOSIS ORDER: (1) reproduce the exact precondition by hand before touching PAM - `PAM_USER=<user> /usr/lib/bunker/<helper> verify <group>`: exit 0 means membership is fine, exit 1 with the 'not a member' message identifies the cause in one command; (2) restore congruence: deploy the daemon version whose spawn path performs the group-membership stage, or uninstall the PAM block with the documented provisioning command (never hand-delete only the config drop-in - the remaining pam_exec precondition fails closed and locks EVERY patterned session out); (3) make skew impossible: provisioning must refuse or loudly warn when the installed daemon is older than the hardening it installs (a version/marker check in the provision command), and spawn must add the group membership BEFORE the agent is reported ready.\n\nRED HERRING TO AVOID: a plausible-looking FUSE hypothesis ('the CLI passes -o allow_other and the host lacks user_allow_other in /etc/fuse.conf') is REFUTED by A/B on the same agent: with allow_other AND without it, sshfs fails identically; the raw SFTP channel is clean (no MOTD pollution); and a hand-run ssh to the same agent returns 254 with no command output. Always check the sshd PAM journal line before blaming the FUSE options.", "environment": "Ubuntu 24.04 + sshd + PAM; a hardening package that installs a fail-closed PAM block for a reserved user pattern (bunker-* agents), plus a per-agent private /tmp via pam_namespace", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "pam-exec-precondition-dies-ssh-session-for-group-members", "provider": "openrouter", "solved_at": "2026-09-16T16:00:28.280Z", "version": "bunker main (tick 439 probe), host-provision drop-in dated 2026-09-13"}Written to /workspace/solution.md (360 lines, self-contained runbook).
Root cause — version skew, not a user error
The host has the SSH-side hardening but the deployed bunkerd predates the spawn-side half. Trace through the installed block:
session [success=2 auth_err=ignore default=die] pam_succeed_if.so quiet user !~ bunker-*
session [success=ignore default=die] pam_exec.so quiet /usr/lib/bunker/pam-tmp-guard verify bunker-agents
session required pam_namespace.so
For a bunker-* user, pam_succeed_if is false → pam_exec runs the fail-closed helper → helper exits 1 (user <u> is not a member of bunker-agents) → pam_exec returns PAM_SYSTEM_ERR → default=die denies the session. sshd reports the generic pam_open_session(): System error; sshfs' SFTP channel is torn down mid-handshake. Non-pattern users are jumped over by success=2, which is why only agents break.
The old daemon spawned agents without running the isolation-provision group stage, so every agent it created was outside bunker-agents.
One-command triage (before touching PAM)
PAM_USER=<agent> /usr/lib/bunker/pam-tmp-guard verify bunker-agents; echo rc=$?
# rc=1 + "not a member" => this bug
Exact fix
bunkerd whose spawn path adds the group before ready, restart it, then back-fill existing agents (no agent restart needed — group membership resolves at each session open):
sh
getent passwd | awk -F: '$1 ~ /^bunker-/ {print $1}' | while read -r u; do
id -nG "$u" | tr ' ' '\n' | grep -qx bunker-agents || usermod -aG bunker-agents "$u"
donepam_exec precondition fails closed again)./usr/share/bunker/hardening.min-daemon-version + strings bunkerd | grep isolation-provision-group) that refuses a too-old daemon; and spawn must usermod -aG bunker-agents + verify before reporting ready.Verification — re-run the helper (expect rc=0 for all agents), then ssh -o BatchMode=yes <agent>@host 'echo SSH_OK; id -nG' (expect rc=0, not 254), plus scp/sshfs and a clean journalctl -u ssh | grep pam_open_session. The document also preserves the fail-closed behavior for genuine non-members and documents the refuted allow_other//etc/fuse.conf and MOTD red herrings.
# Evidence - Problem class: pam-exec-precondition-dies-ssh-session-for-group-members - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-16T16:00:28.280Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: every SSH session for users matching the reserved pattern dies immediately after successful public-key auth. sshd journal: 'pam_unix(sshd:session): session opened for user bunker-<agent>(uid=1010)' followed by 'error: PAM: pam_open_session(): System error'. The client sees NO command output and exit status 254; sshfs reports 'remote host has disconnected' / 'read: Connection reset by peer'. So `exec`-over-ssh, `mount` (sshfs) and `cp` all fail against a HEALTHY, running agent while the daemon reports it as running.\n\nMECHANISM: the installed PAM block is\n session [success=2 auth_err=ignore default=die] pam_succeed_if.so quiet user !~ bunker-*\n session [success=ignore default=die] pam_exec.so quiet /usr/lib/bunker/pam-tmp-guard verify bunker-agents\n session required pam_namespace.so\nThe helper fails closed on membership: run with a user who is NOT in the required group it prints 'bunker-pam-guard: user <u> is not a member of bunker-agents' and exits 1. pam_exec maps a non-zero exit to PAM_SYSTEM_ERR, and the control field `default=die` turns that into a denied session - reported by sshd as the generic 'System error'. Because sshfs uses the SFTP subsystem over the same session, its channel is torn down mid-handshake.\n\nWHY IT HAPPENED (version skew, not a user error): the SSH-side half of the hardening was installed on the host by the provisioning command, but the DEPLOYED daemon binary predated the spawn-side half that adds each new agent to the required group (the 'isolation-provision' spawn stage). Host provisioned + old daemon = every agent that daemon spawns is locked out of SSH.\n\nFIX / DIAGNOSIS ORDER: (1) reproduce the exact precondition by hand before touching PAM - `PAM_USER=<user> /usr/lib/bunker/<helper> verify <group>`: exit 0 means membership is fine, exit 1 with the 'not a member' message identifies the cause in one command; (2) restore congruence: deploy the daemon version whose spawn path performs the group-membership stage, or uninstall the PAM block with the documented provisioning command (never hand-delete only the config drop-in - the remaining pam_exec precondition fails closed and locks EVERY patterned session out); (3) make skew impossible: provisioning must refuse or loudly warn when the installed daemon is older than the hardening it installs (a version/marker check in the provision command), and spawn must add the group membership BEFORE the agent is reported ready.\n\nRED HERRING TO AVOID: a plausible-looking FUSE hypothesis ('the CLI passes -o allow_other and the host lacks user_allow_other in /etc/fuse.conf') is REFUTED by A/B on the same agent: with allow_other AND without it, sshfs fails identically; the raw SFTP channel is clean (no MOTD pollution); and a hand-run ssh to the same agent returns 254 with no command output. Always check the sshd PAM journal line before blaming the FUSE options.", "environment": "Ubuntu 24.04 + sshd + PAM; a hardening package that installs a fail-closed PAM block for a reserved user pattern (bunker-* agents), plus a per-agent private /tmp via pam_namespace", "language": "shell", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "pam-exec-precondition-dies-ssh-session-for-group-members", "provider": "openrouter", "solved_at": "2026-09-16T16:00:28.280Z", "version": "bunker main (tick 439 probe), host-provision drop-in dated 2026-09-13"}