Modernizing Authentication: Writing Linux-PAM Modules in GNU Guile

For over three decades, Linux-PAM (Pluggable Authentication Modules) has served as the bedrock of authentication on Linux systems. Its design was revolutionary for its time, allowing security-relevant applications like su or login to follow consistent policies without requiring recompilation whenever a policy changed. However, the reliance on shared C objects often makes the process of writing and maintaining custom modules cumbersome and opaque.

Guile-PAM emerges as a modern alternative, enabling system administrators to write, configure, and maintain PAM modules using GNU Guile, a dialect of Scheme. By bridging the gap between the low-level PAM API and a high-level interpreted language, Guile-PAM transforms authentication logic from static compiled binaries into flexible, readable scripts.

The Architecture of Guile-PAM

At its core, Guile-PAM operates via a specially crafted shared object, pam_guile.so. This object acts as a gateway, calling GNU Guile scripts to handle authentication tasks. This architecture allows for three distinct levels of integration:

  1. Task-Specific Modules: Using pam_guile.so as a standard Linux-PAM shared object to perform a single specific task in Guile.
  2. Service Replacement: Replacing an entire PAM recipe (a specific service) by making pam_guile.so the sole shared object and handling all logic within Guile.
  3. System-Wide Orchestration: Replacing all services in /etc/pam.d with pam_guile.so, effectively moving both recipe selection and logic into the Guile environment.

Understanding "Pamdas"

In Guile-PAM, the fundamental unit of logic is the Pamda (a portmanteau of PAM and Lambda). A Pamda is an anonymous procedure with the following signature:

(lambda (action handle flags options) ...)
  • Action: One of six symbols (e.g., pam_sm_authenticate, pam_sm_open_session) representing the PAM entry point.
  • Handle: An opaque variable providing access to internal PAM data, such as the username and the conversation function.
  • Flags: An integer bitmask.
  • Options: A list of strings passed from the service definition.

Reimplementing the PAM Stack

One of the most powerful features of Guile-PAM is its complete reimplementation of the Linux-PAM stack in Scheme. Instead of relying on the static configuration files of Linux-PAM, developers can define "gates" and "plans" programmatically.

Gates and Plans

A gate procedure wraps a pamda and assigns it a plan (such as required, optional, sufficient, or requisite). The stack procedure then evaluates these gates in order.

For those migrating from traditional setups, Guile-PAM provides control-string->plan, which translates standard Linux-PAM control strings (e.g., success=ok ignore=ignore default=bad) directly into Guile-PAM plans.

Hybrid Stacks

Guile-PAM does not require a total abandonment of existing C-based modules. Using call-shared-object, a Guile-PAM stack can invoke legacy .so files. To prevent stability issues—since many legacy modules return PAM_SERVICE_ERR when called for actions they don't implement—Guile-PAM allows developers to specify exactly which actions a shared object implements using the #:implements keyword.

Advanced Use Cases: Beyond Simple Auth

Because Guile-PAM is an interpreted language with access to the system, it can achieve things that are difficult or impossible with standard PAM modules.

Example: User-Level Mounting

A primary motivation for Guile-PAM was the ability to mount volumes as the actual user rather than as root. While pam_mount.so exists, it typically mounts as root, which fails with FUSE folders or kerberized NFSv4.

Guile-PAM enables a workflow where a token received during pam_sm_authenticate is stored and then used during pam_sm_open_session to pipe a secret into a user-space tool like gocryptfs, mounting the home directory as the user during the login process.

Security and Performance Considerations

Moving authentication logic to an interpreted language introduces specific considerations:

Security

  • Readability: The author argues that Scheme code is generally easier to audit than C, increasing the likelihood of peer review.
  • Injection Risks: To prevent attacks, developers are cautioned against using Guile's read procedure on unknown inputs.
  • Environment Isolation: pam_guile.so isolates environment variables like GUILE_LOAD_PATH to prevent unauthorized module substitution.

Performance

Practical tests indicate that Guile-PAM performs at adequate speeds. Interestingly, the author notes that extreme speed is not always a goal in authentication, as intentional delays are often built-in to thwart brute-force attacks.

Implementation Caveats

Users should be aware that Guile-PAM is currently in the alpha stage. There are key behavioral differences compared to mature Linux-PAM:

  • Action Grouping: Linux-PAM groups certain actions (like auth and setcred) and may skip the second if the first returns PAM_IGNORE. Guile-PAM's (pam stack) implementation evaluates every module for every action.
  • Skip Counts: In legacy instruction sets with explicit skip counts, an unlocked Guile-PAM stack always acquires the result, whereas Linux-PAM's side effects vary by action.

Conclusion

Guile-PAM represents a shift toward more transparent and flexible system administration. By treating authentication as a programmable logic problem rather than a configuration of binary blobs, it opens the door to novel branching strategies and deeper integration with other free-software systems, such as OpenBSD scripts. For those using GNU Guix, the integration is even more seamless, allowing the entire authentication stack to be defined within the system configuration.

Sources