Enforces the ism-1558 minimum word count for passwords that are a sequence of words: at least 4 words on non-classified, OFFICIAL: Sensitive and PROTECTED systems, 5 on SECRET and 6 on TOP SECRET. Single-token passwords are passed through untouched, which is what lets ism-1558 and ism-2080 hold together.
The default implementation is pam_ism_wordcount.so, a purpose-built PAM module that counts whitespace-delimited words in one read-only, in-process pass. A pam_exec(8) shell equivalent is shipped as a fallback for hosts that cannot deploy a compiled module.
Depends on debian-13-pwquality-base. The check reads the new password but never asks for it; pam_pwquality, earlier in the stack, obtains it. Without it pam-auth-update still gives pam_unix use_authtok, there is no token to use, and every password change fails: verified on a clean Debian 13 install. The apply script refuses to run without pam_pwquality in the stack.
The ISM word-count requirement, as published in the ACSC ISM OSCAL catalogue release v2026.09.4.
ISM-1558 — Password strength
Partially implemented. This component enforces the word-count limb. Sequences, repeats and account-derived strings are rejected by debian-13-pam-pwquality; the provenance limbs are not enforced by any component.
ENFORCED, verified by behaviour through the PAM stack on all three applicability tiers: a password using a sequence of words must use at least 4 words on non-classified, OFFICIAL: Sensitive and PROTECTED systems, 5 on SECRET and 6 on TOP SECRET. This is implemented by pam_ism_wordcount.so, a purpose-built PAM module that counts whitespace-delimited words in a single read-only pass. It runs in-process, so the candidate never leaves the PAM process. A pam_exec(8) shell equivalent is shipped as a fallback for hosts that cannot deploy a compiled module; pam_exec ships in libpam-modules, so neither route requires an additional package.
Crucially, the check applies ONLY to passwords that are a sequence of words. A single-token password is not one, so it is passed through untouched. That is what allows this control and ism-2080 to hold simultaneously; see the remarks for why a character-class module cannot.
NOT ENFORCED: a sequence of categorised words, a real sentence in natural language, and song lyrics, quotes or other publicly available material all satisfy a word count while being exactly what this control prohibits. Verified: all three are still accepted. Determining whether words were randomly selected is not something any available module can do.
Residual risk Word count is enforced; randomness and provenance of the words are not. Natural-language sentences, lyrics and categorised word lists remain accepted.
Remarks
Why not pam_passwdqc: it can enforce a word count, but passwdqc requires each min value to be no larger than the preceding one, so 'disabled' can only ever apply to the weakest character-class tiers. It can therefore never express 'reject all non-passphrases regardless of class'. Tested: under min=disabled,disabled,15,15,15 with passphrase=4 a two-word candidate is correctly rejected, but a 15-character single-token password is ALSO rejected -- which breaches ism-2080, since ism-1558 governs only passwords using a sequence of words. passwdqc's min is additionally an effective length rather than a raw character count, so it cannot assert the literal 'minimum of 15 characters' that ism-0421 states.
The pam_exec approach keys off word structure instead, which is what the control actually describes, and leaves single-token passwords alone.
Implementation choice: a purpose-built C module is the default because it removes the only real exposure of the shell route, where expose_authtok passes the cleartext candidate across a pipe to a child process. The module reads PAM_AUTHTOK in-process and the candidate is never copied, logged, or passed to another process.
C rather than Rust: the check is a single read-only pass counting whitespace transitions, with no allocation, no copying and no index arithmetic, so the memory-safety hazards C is criticised for are absent rather than merely managed. A Rust module would still require #[no_mangle] extern "C" and an unsafe read of a raw *const c_char from pam_get_item, placing unsafe code at exactly the point of risk, while adding a rustc build dependency and a much larger artefact. The source is 91 lines and compiles clean under -Wall -Wextra -Werror; the resulting object is roughly 15 kB and links only libpam and libc.
Alternatives evaluated and rejected: libpam-python would also run in-process but depends on libpython3.13 and installs 11 packages including the full CPython standard library, linking a Python interpreter into sshd, login, sudo and cron; its Debian changelog records 'Fix CVE-2019-16729 again' in 2024, five years after the original. libpam-script uses the same external script model as pam_exec and so changes nothing.
The pam_exec fallback, where used, is hardened: pinned to type=password, ulimit -c 0 so a credential cannot reach a core file, umask 077, the shell read builtin rather than a command substitution so no second process holds the cleartext, and no branch that logs or passes the candidate as a command argument.
Residual mitigation: pwqgen(1) or pwmake(1) generate passphrases so users never select words themselves, which is the only practical answer to the provenance limbs.
Configuration and scripts