Skip to content
FreeBSDDeep Dive Published Updated 6 min readViews unavailable

FreeBSD pam_exec Policy Helpers: Account Checks, Stack Ordering, and Lockout Recovery

Design service-specific FreeBSD PAM helpers with account-stage checks, explicit exit semantics, bounded execution, protected files, and lockout recovery.

A PAM helper can add a narrow operational policy to an existing authentication service, such as denying new sessions during a maintenance interval. FreeBSD’s pam_exec module runs an external program from a PAM chain. The difficult part is not launching the program: it is placing the check in the correct facility, choosing control semantics, keeping the helper trustworthy, and preserving access when the helper fails.

This article uses the FreeBSD 15.0 source-tree manual and implementation as its versioned reference. FreeBSD uses OpenPAM; Linux-PAM configuration examples and module options are not automatically interchangeable. The example below is an account-policy check for a dedicated test service. It is not a complete login policy and must not replace a working sshd, login, or su policy wholesale.

Match the policy to a PAM facility

PAM separates authentication, account management, credential/password management, and session handling. A maintenance rule about whether an already identified account may start a session belongs naturally in account management. It does not need to inspect a password. Authentication establishes identity; an account-stage restriction can then decide whether that identity is currently permitted for the service.

The application must actually call the relevant PAM facility. Adding an account line cannot affect an application that never invokes account management. Likewise, a command that performs a PAM conversation under one service name may load a different policy from the application you intend to restrict. Inventory the service’s actual PAM integration before writing the rule.

Use a dedicated laboratory host and test identity first. Read the existing policy, including every include line, and map the order of modules in each facility. Keep a privileged console session and a known restoration copy available. A new SSH connection is a stronger test than continuing to type commands into a session that was authenticated before the change.

Understand helper inputs and exit results

FreeBSD pam_exec exports selected PAM items such as PAM_USER, PAM_SERVICE, and PAM_RHOST, plus PAM_SM_FUNC, which identifies the module function being called. Those values describe the request context; they are not an authorization decision by themselves. Treat values supplied through an authentication interaction as untrusted input and avoid interpolating them into shell commands.

By default, a program exit status of zero maps to PAM_SUCCESS; nonzero maps to PAM_PERM_DENIED. The optional return_prog_exit_status mode instead interprets the exit status as an allowed PAM code for the calling function, rejecting inappropriate values. Keep the default mode for a simple binary permit/deny helper. An ordinary Unix error number is not inherently a valid PAM return value.

The module can expose the authentication token to the helper’s standard input through expose_authtok. An account maintenance check does not need that option. Capturing stdout or stderr can send output into the application’s conversation function; avoid using those channels for secrets or assuming every service displays messages identically.

A narrow local account-policy example

The following illustrative helper permits only the expected account-stage invocation for a test service called policy-lab, and denies access when a root-controlled maintenance marker exists. Save it as a root-owned, non-user-writable file such as /usr/local/libexec/pam-policy-lab on the test host:

#!/bin/sh
PATH=/bin:/usr/bin
export PATH

[ "${PAM_SERVICE-}" = policy-lab ] || exit 1
[ "${PAM_SM_FUNC-}" = pam_sm_acct_mgmt ] || exit 1
[ -n "${PAM_USER-}" ] || exit 1

if [ -e /var/db/policy-lab-maintenance ]; then
    exit 1
fi
exit 0

The script intentionally does not evaluate user-provided text, query a network service, or decide who counts as an administrator. Protect its parent directories and the marker’s parent directory as well as the script itself. If an unprivileged user can replace the executable or create the marker, that user can change policy or deny service. Use an absolute interpreter and helper path, and inspect the deployed file’s ownership and mode.

In a service-specific PAM file, a conceptual account-chain fragment is:

account required pam_exec.so /usr/local/libexec/pam-policy-lab

Retain the service’s existing account checks. This fragment adds a requirement; it does not perform Unix account-expiry validation or create an authentication chain. A dedicated PAM test client must use service name policy-lab and invoke pam_acct_mgmt. Merely executing the shell script from a terminal exercises its own logic, not OpenPAM chain evaluation.

Place the requirement where the chain will execute it

A required module failure contributes to the chain’s final failure while allowing subsequent processing. A requisite failure stops processing immediately. A successful sufficient module can terminate a chain under the documented conditions. Consequently, placing a mandatory helper after an earlier success shortcut may leave that helper unevaluated.

Read the entire expanded chain rather than reviewing the new line in isolation. In particular, an included policy can contain control decisions that change whether later checks run. A permissive optional helper is suitable for some informational tasks but should not be described as enforcing a mandatory restriction.

Keep policy responsibilities small. If the helper starts managing passwords, deciding group membership, consulting multiple APIs, and recording long session state, the external script has become an authentication subsystem with a larger failure surface. Prefer a dedicated supported module or an application-native authorization mechanism when the requirement exceeds a simple local decision.

Make execution time part of the access policy

The FreeBSD implementation waits for the executed helper. A helper that hangs on DNS, an unavailable filesystem, or an external service can delay the calling authentication operation. Do not assume PAM configuration provides an automatic timeout for arbitrary programs. The local marker example avoids network dependencies and performs a bounded amount of work.

If a production helper genuinely needs a remote decision, design an explicit deadline and document the outage behavior. Test connection failure, slow response, malformed response, and timeout. Decide whether unavailability denies the request or uses a controlled local fallback; neither outcome should arise accidentally from an unchecked command exit status. Also consider concurrent login attempts and whether the helper creates a bottleneck or amplifies load on an already failing service.

Logging should report the policy decision and a safe reason code without capturing credentials or unsanitized user strings. A log statement is not the policy result: test that the helper’s exit status and the application’s final decision agree. Avoid debug options whose behavior differs across PAM implementations, and never infer success from a banner alone.

Verify negative cases and restoration

Test a normal permitted request, marker-enabled denial, missing executable, non-executable file, wrong service, and wrong facility invocation. Exercise the actual application flow as well as the helper directly. Confirm a failed helper cannot be bypassed by a later module, and that a preexisting privileged session remains available for restoration.

When a rollout fails, restore the reviewed PAM policy through the console and verify a new session before closing recovery access. Remove temporary test-service files only after the application has returned to its expected configuration. A service restart may be required by the application, but PAM does not justify restarting unrelated authentication services indiscriminately.

Record the FreeBSD release, module version, service name, expanded chain, deployed helper checksum, allowed and denied test cases, and restoration procedure. Recheck that evidence after OS or application updates. A reliable pam_exec policy is one whose positive path, denial path, execution bounds, file ownership, and recovery path have all been observed through the real service.

Related:

Sources:

Comments