Run a Controlled PAM Hook with pam_exec
You will finish with a PAM configuration line that runs one fixed, executable helper, records the PAM context it received, and reports failure without accidentally handing a password to a shell script. The examples match pam_exec from Ubuntu's libpam-modules package, version 1.5.3-5ubuntu5.7 installed here.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about twenty minutes, plus a maintenance window if you are changing a real login, SSH or password stack. You need root access to install the helper and edit a file under /etc/pam.d. PAM changes are security-sensitive and can lock you out, so keep an existing root shell or console session open while testing.
1. Check the installed module and choose a narrow event
These are ordinary, read-only checks:
$ dpkg-query -W -f='${Package} ${Version}\n' libpam-modules:amd64
libpam-modules 1.5.3-5ubuntu5.7
$ man 8 pam_exec
$ man 5 pam.d
A PAM service file contains a management type, a control flag, a module and its arguments. For example, the file /etc/pam.d/passwd is used by the passwd service, so a line beginning password belongs to password changes. The available module types are auth, account, password and session.
Pick the one event your helper actually needs. A notification after a password change belongs in a password stack. A check before access belongs in an authentication or account stack. Do not add the line to several stacks just to make sure it runs: that creates repeated side effects and makes failures harder to diagnose.
Checkpoint
Write down the exact service file and event, such as /etc/pam.d/passwd and password. Do not begin with a shared file such as common-auth unless you deliberately want the hook to affect every service that includes it.
2. Create a fixed helper with a clear exit status
The helper must be an absolute path. pam_exec does not turn a command string into a safe policy boundary, and the child environment can contain values controlled by the user. Avoid sh -c, PATH lookups and unquoted data. Make the helper accept only the inputs it needs and return zero for success:
#!/bin/sh
set -eu
log_file=/var/log/pam-account-hook.log
printf 'service=%s type=%s user=%s\n' \
"${PAM_SERVICE-}" "${PAM_TYPE-}" "${PAM_USER-}" >> "$log_file"
exit 0
Install a real version of that file as root, with an owner and mode that prevent ordinary users from replacing it:
# install -o root -g root -m 0755 /tmp/pam-account-hook /usr/local/sbin/pam-account-hook
# install -o root -g root -m 0600 /dev/null /var/log/pam-account-hook.log
# sh -n /usr/local/sbin/pam-account-hook
# ls -l /usr/local/sbin/pam-account-hook /var/log/pam-account-hook.log
Use a log destination with suitable permissions, or write to the system journal through a purpose-built helper. Never log PAM_AUTHTOK, passwords, session tokens or the complete environment. The helper's output is normally discarded by pam_exec, but its exit status still matters to the PAM control flag.
Checkpoint
Verify that the helper is root-owned, executable and passes shell syntax checking. The install commands above change state and require elevated privileges. The temporary source file is only an example; replace it with the reviewed helper you intend to deploy.
3. Add the smallest possible PAM rule
Make a backup before editing the service file. This is reversible, but a malformed or badly placed rule can disrupt authentication:
# cp -p /etc/pam.d/passwd /etc/pam.d/passwd.pam-exec-backup
Edit /etc/pam.d/passwd and add this line at a deliberate point in the existing password stack:
password optional pam_exec.so type=password /usr/local/sbin/pam-account-hook
optional means the hook's result is normally not the deciding result when other modules are present. It is a reasonable starting point for a notification or audit action. Use required only when a failed helper must make the PAM operation fail, and understand that a helper outage can then block password changes or access.
The type=password argument restricts execution to the password module type. The module also supports debug, quiet, quiet_log, stdout, log=file, seteuid and expose_authtok. Leave the last option out. It sends the authentication password to the command's standard input and is not needed for this logging example.
By default the command runs with the real user ID of the calling process. seteuid changes that to the effective user ID, which can materially change file access. Add it only after checking the helper's privilege requirements. The log= option appends command output to a file; stdout instead sends output to the calling application and makes log= ineffective. Do not use either as a substitute for structured, access-controlled logging.
4. Test the stack without closing your safety net
There is no standalone pam_exec command to run. A PAM-aware application invokes the module. For the example above, use passwd only when you are prepared to complete a password change, and keep a separate root session open. If the service is SSH, validate the file and test a new connection before ending the existing one.
After a successful password change, inspect only the expected record:
$ sudo tail -n 1 /var/log/pam-account-hook.log
service=passwd type=password user=EXAMPLE_USER
The username and exact service are host-specific. The module exports PAM_RHOST, PAM_RUSER, PAM_SERVICE, PAM_TTY, PAM_USER and PAM_TYPE in the child environment, alongside the current PAM environment list. Treat all of them as untrusted input. A helper should use an allow-list and careful quoting, not build a shell command from them.
If the helper fails, the PAM module returns a system error. Without quiet, pam_exec can echo a failed command's exit status; without quiet_log, it can log that failure. These messages are diagnostics, not proof that the surrounding PAM stack will deny the operation: the control flag decides how the stack treats the module result.
5. Recover cleanly from a bad hook
Warning
Do not experiment by locking the only administrator out. If a test breaks the service, use the still-open root console and restore the backup:
# cp -p /etc/pam.d/passwd.pam-exec-backup /etc/pam.d/passwd
# rm -f /etc/pam.d/passwd.pam-exec-backup
Removing the rule is the normal undo operation. Remove the helper and its log only after confirming that no other PAM service uses them:
# rg -n 'pam-account-hook|pam_exec' /etc/pam.d
# rm -f /usr/local/sbin/pam-account-hook /var/log/pam-account-hook.log
Do not use expose_authtok as a quick fix for missing context. If a helper genuinely needs authentication data, review its process permissions, input handling, memory lifetime and logging before changing the PAM rule. A short password on standard input is still a secret.
Done means
- The installed
libpam-modulesversion and target PAM service are recorded. - The helper uses an absolute path, is root-owned, executable and passes syntax checks.
- The rule uses the narrowest event and an intentional control flag.
- The helper handles PAM environment values as untrusted input and never logs passwords.
- A real PAM-aware test produced the expected service, type and user record.
- A backup or console recovery path remains available before the change is rolled out.