Understanding Pluggable Authentication Module and Creating a Custom One in Python

In today's interconnected world, user authentication plays a critical role in ensuring the security and integrity of computer systems. Whether it's logging into an application, accessing sensitive data, or protecting digital assets, effective user authentication is essential to verify the identity of individuals accessing these resources.

One powerful tool in the realm of authentication is the Pluggable Authentication Module (PAM). PAM provides a flexible framework for implementing authentication mechanisms in various Unix-like systems, allowing system administrators to integrate multiple authentication methods seamlessly.

In this blog post, we will delve into the intricacies of PAM, exploring its architecture, modules, and control flags. We will understand how PAM separates the authentication process from individual applications, providing a centralized and standardized approach to authentication.

Furthermore, we will take our understanding of PAM a step further by demonstrating how to develop a custom PAM module using Python. With Python's simplicity and versatility, we can create a module that extends the authentication capabilities of our system, enabling us to implement custom authentication logic to meet our specific requirements.

We will walk through the process of developing a Python-based PAM module, discussing the code, its functionality, and the configuration options available. Additionally, we will explore the steps for integrating our custom module into the PAM system and provide practical guidance for configuring and using the module effectively.

By the end of this blog post, you will have a comprehensive understanding of PAM, the ability to create custom PAM modules, and the knowledge to enhance authentication solutions in your system. So, let's dive in and unravel the world of PAM and custom authentication modules!

An Overview of PAM

What is PAM?

Pluggable Authentication Modules, or PAM, are dynamic libraries that provide a generic interface for authentication-related tasks. It effectively separates the specifics of authentication from applications, allowing system administrators to customize authentication procedures without changing or rewriting the software.

By using PAM, a system can implement a range of authentication methods including, but not limited to, password, biometric, or hardware-based authentication. PAM's flexibility and extensibility give it a significant advantage over hard-coded authentication mechanisms, ensuring the system remains robust and adaptable to emerging security challenges.

When an application needs to authenticate a user, it can invoke PAM as an authentication module. Here's an explanation of how the application interacts with PAM during the authentication process:

  1. Application Initialization: The application initializes the authentication process, typically when a user attempts to log in or access a protected resource. It identifies that user authentication is required.
  2. Application-PAM Interaction: The application interacts with PAM by calling PAM functions or APIs provided by the operating system. These functions allow the application to initialize the PAM authentication process and pass relevant information, such as the username and authentication credentials, to PAM.
  3. PAM Stack Invocation: Once the application interacts with PAM, the PAM layer comes into play. PAM invokes a predefined stack of modules based on the configuration for the specific service or application. The stack defines the order and types of modules to be executed during the authentication process.
  4. Authentication Modules: The first set of modules to be executed within the PAM stack are the authentication modules. These modules are responsible for verifying the user's identity by validating the provided credentials, such as a password, biometric data, or a cryptographic key. Each authentication module performs its verification process, and the results are passed back to the PAM layer.
  5. Account Modules: After the authentication modules, the account modules are invoked. These modules check whether the authenticated user has the necessary permissions or access rights to use the application or access-specific resources. They enforce any account-related policies or restrictions defined in the configuration.
  6. Session Modules: The session modules come into play once the user's authentication and account have been validated. These modules handle the setup and management of the user's session, ensuring that resources are allocated, environment variables are set, and any necessary actions are performed to initialize the user's session.
  7. Password Modules: If the user successfully passes the previous modules, the password modules are invoked. These modules manage password-related tasks, such as password change or updating mechanisms. They enforce password policies, handle password encryption, and perform any necessary actions to maintain the security of user credentials.
  8. PAM Result: Throughout this process, PAM keeps track of the results from each module. Based on the results, PAM determines whether the authentication process is successful or not. The final result is passed back to the application, indicating whether the user is authenticated or denied access.

By invoking PAM as an authentication module, the application leverages the flexibility and extensibility of the PAM framework. It delegates the responsibility of authentication to the PAM layer, allowing for centralized and customizable authentication processes across different applications and services.

Dive into PAM Modules and Stacks

PAM employs a modular approach to authentication. Each PAM module is essentially a shared library, written in C or another supported language, which implements a specific authentication mechanism. Some common examples of PAM modules include pam_unix.so for traditional password authentication, pam_cracklib.so for password strength checking, and pam_nologin.so for denying logins when /etc/nologin exists.

Each of these modules performs specific tasks by implementing different interfaces. Some modules implement all of these interfaces (pam_unix.so, pam_systemd.so, etc). These interfaces are executed as various stages of the authentication process. such as verifying a user password (auth), providing password changing function (password), setting up user credentials (session), or establishing account management commands (account).

PAM uses a concept called “stacks” to organize these modules for each service that requires authentication. A PAM stack is essentially an ordered list of module entries, read from top to bottom. For each module entry, PAM will execute the corresponding module and depending on its success or failure, and the control flag associated with it, will continue, terminate, or skip other modules.

Control flags play a pivotal role in controlling the authentication flow. There are four types of control flags:

Let's visualize this with a hypothetical PAM stack for the login service:

# /etc/pam.d/login
auth       requisite      pam_securetty.so
auth       required       pam_unix.so nullok
auth       optional       pam_group.so
account    required       pam_unix.so
password   required       pam_cracklib.so
password   required       pam_unix.so obscure sha512
session    required       pam_unix.so

This PAM configuration you've provided is related to the login service on a Unix-like system. It contains four types of module-types: auth, account, password, and session. Here's a breakdown:

  1. auth: Determines how a user will prove their identity. In this case, there are three modules:
    • pam_securetty.so: It restricts root access to the system through devices that aren't listed in the /etc/securetty file. “Requisite” here means that if this module fails, the entire authentication process will be dropped immediately, presenting a failure message to the user.
    • pam_unix.so: It is a standard module for traditional password authentication. It uses standard UNIX password encryption. The nullok argument means it's okay if the password field is empty; it won't cause an authentication failure.
    • pam_group.so: It is used to grant additional group memberships based on the user's ID, service, and terminal. It is only included as optional
  2. account: Manages user account properties, usually related to access controls and resource usage.
    • pam_unix.so: In the context of the account type, it checks for password expiration, account expiration, and whether the user is allowed to access the system at the current time.
  3. password: Deals with password management.
    • pam_cracklib.so: It checks the strength of a password against a library of known weak passwords.
    • pam_unix.so: It handles password updates. The obscure argument checks for simple passwords, and sha512 indicates that the updated passwords should be hashed using the SHA-512 algorithm.
  4. session: Configures and manages user sessions.
    • pam_unix.so: It handles session-related tasks such as mounting the user's home directory and logging session opening/closing.

Remember that PAM modules are processed in the order they're written in the configuration file. The flow stops either when a module fails (if it's marked as requisite or required) or at the end of the module list (if all the modules are marked as optional). In this case, if pam_securetty.so fails, the user isn't authenticated, no matter the result of the other two auth modules.

This modular design gives PAM immense flexibility, allowing you to choose authentication policies that best suit your system's security needs.

The ability to customize the control flow via these flags allows for a granular level of control over the authentication process, making PAM an adaptable and powerful framework for system security.

Developing a Custom PAM Module

As we delve deeper into the world of PAM, it becomes clear that one of its standout features is the ability to develop custom modules. This functionality not only allows for system-specific authentication methods, but also enables rapid development and integration of new security mechanisms as the need arises. This way, you can seamlessly adapt your authentication strategies to meet evolving security challenges or unique system requirements.

However, it's worth noting that with this flexibility and adaptability comes an increased responsibility. When developing custom PAM modules, care should be taken to ensure that security best practices are followed. After all, the code you write will form a part of the system's authentication mechanism. In particular, the source code for the modules should be guarded carefully. If an attacker gains access to this code, they could potentially modify it to circumvent the entire authentication process.

In this section, we will demonstrate how to create a custom PAM module using Python – a language chosen for its ease of use and wide range of libraries. Our module will extend the system's existing authentication capabilities, showcasing the flexibility and adaptability that makes PAM a powerful tool in the hands of system administrators and security professionals.

What Makes a PAM Module?

A PAM module is essentially a shared library that provides one or more of four basic service types: authentication management (auth), password management (password), account management (account), and session management (session).

In a PAM module, there are several interfaces that correspond to different stages of an authentication process. These stages include setting up credentials, managing user accounts, managing user sessions, and changing authentication tokens. For each of these stages, there's a corresponding function that the module needs to implement:

Let's take a closer look at a simple PAM module written in Python. The module will perform basic password authentication, comparing a user's input against a stored password:

import spwd
import crypt

def pam_sm_authenticate(pamh, flags, argv):
    try:
        entered_password = pamh.authtok
        stored_password = spwd.getspnam(pamh.user)[1]
    except Exception as e:
        return pamh.PAM_AUTH_ERR

    if crypt.crypt(entered_password, stored_password) != stored_password:
        return pamh.PAM_AUTH_ERR

    return pamh.PAM_SUCCESS

def pam_sm_setcred(pamh, flags, argv):
    return pamh.PAM_SUCCESS

def pam_sm_acct_mgmt(pamh, flags, argv):
    return pamh.PAM_SUCCESS

def pam_sm_open_session(pamh, flags, argv):
    return pamh.PAM_SUCCESS

def pam_sm_close_session(pamh, flags, argv):
    return pamh.PAM_SUCCESS

def pam_sm_chauthtok(pamh, flags, argv):
    return pamh.PAM_SUCCESS

In this example, pam_sm_authenticate is the main function of interest. It takes three arguments: pamh (the PAM handle, which is an object encapsulating all PAM-related data), flags (which can be used to modify the module's behavior), and argv (the arguments passed to the module).

This function fetches the entered password from the PAM handle and the stored password for the given user from the system's shadow password database. It then uses the crypt library to hash the entered password with the salt from the stored password. If the hashed entered password doesn't match the stored password, the function returns PAM_AUTH_ERR, indicating an authentication error. Otherwise, it returns PAM_SUCCESS, indicating successful authentication.

The other functions (pam_sm_setcred, pam_sm_acct_mgmt, pam_sm_open_session, pam_sm_close_session, and pam_sm_chauthtok) are required to complete the PAM module. In this simple example, they don't do anything and simply return PAM_SUCCESS. In a more complex module, these could handle tasks like setting user credentials, managing user accounts, opening and closing sessions, and changing authentication tokens (passwords), as mentioned above.

This example should give you a basic idea of what a PAM module looks like. From here, you can extend and modify this template to create a PAM module that meets your specific needs.

Modifying the template to a custom PIN Login

Let's explore our custom Python PAM module. This module uses a custom PIN for authentication. Here's the complete code:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import json

def _get_config(argv):
    config = {}
    for arg in argv:
        argument = arg.split('=')
        if len(argument) == 1:
            config[argument[0]] = True
        elif len(argument) == 2:
            config[argument[0]] = argument[1]
    return config

def _get_pin_file(config):
    try:
        pin_file = config['pin_file']
    except:
        pin_file = "$HOME/.config/pin"
    return pin_file.replace('$HOME',os.environ.get('HOME'))

def _get_max_tries(config):
    try:
        max_tries = config['max_tries']
    except:
        max_tries = 0
    return max_tries

def pam_sm_authenticate(pamh, flags, argv):
    config = _get_config(argv)

    try:
        user = pamh.get_user()
    except pamh.exception, e:
        return e.pam_result

    if not user:
        return pamh.PAM_USER_UNKNOWN

    try:
        resp = pamh.conversation(pamh.Message(pamh.PAM_PROMPT_ECHO_OFF,
                                 'Password: '))
    except pamh.exception, e:
        return e.pam_result

    max_tries = _get_max_tries(config)
    pin_file = _get_pin_file(config)

    if not os.path.exists(pin_file):
        return pamh.PAM_AUTH_ERR

    creds = json.load(open(pin_file))
    if creds['tries'] <= max_tries or max_tries == 0:
        if creds['pin'] == resp.resp:
            creds['tries'] = 0
            json.dump(creds, open(pin_file, 'w+'))
            return pamh.PAM_SUCCESS
        else:
            creds['tries'] += 1
            json.dump(creds, open(pin_file, 'w+'))

    return pamh.PAM_AUTH_ERR

def pam_sm_setcred(pamh, flags, argv):
    return pamh.PAM_SUCCESS

def pam_sm_acct_mgmt(pamh, flags, argv):
    return pamh.PAM_SUCCESS

def pam_sm_open_session(pamh, flags, argv):
    return pamh.PAM_SUCCESS

def pam_sm_close_session(pamh, flags, argv):
    return pamh.PAM_SUCCESS

def pam_sm_chauthtok(pamh, flags, argv):
    return pamh.PAM_SUCCESS

The main function here is again pam_sm_authenticate. It reads the module configuration from the arguments passed to it. The configuration parameters include the path to the PIN file (pin_file) and the maximum number of authentication attempts allowed (max_tries).

The PIN file is a JSON file storing the correct PIN and the number of unsuccessful authentication attempts made so far. If this file doesn't exist or can't be read, the function immediately returns an authentication error.

Following that, the function retrieves the user's credentials. If the user doesn't exist, it instantly returns an error signaling an unknown user.

The function then prompts the user to enter their PIN. If the PIN matches the one stored in the file and the number of unsuccessful attempts has not exceeded the maximum allowed, the function resets the count and returns PAM_SUCCESS, signifying successful authentication. If the entered PIN doesn't match the stored one, the function increments the count of tries and returns PAM_AUTH_ERR, indicating an authentication error.

This custom PAM module adds a second factor of authentication, or simple PIN Authentication with a custom user configurable PIN. By leveraging Python's innate support for JSON and OS interfaces, this demonstrates the potential and flexibility that custom PAM modules can provide.

Integration and Usage

Now that we have our Python-based PAM module ready, the next step is integrating it into our system and putting it to use.

Pre-requisites

To use our custom Python PAM module, we must satisfy a few pre-requisites:

Placing the Module

Copy the Python PAM module script to a suitable location. For instance, you can place it in /usr/local/lib/security/. Make sure the permissions are set correctly, so that the module is readable by the services that will use it.

sudo cp my_module.py /usr/local/lib/security/
sudo chmod 644 /usr/local/lib/security/my_module.py

Configuring PAM

The PAM library determines the sequence and execution of the PAM modules through the configuration files. Each service using PAM has its own configuration file. On most Unix-like systems, these are stored in the /etc/pam.d/ directory. The configuration files use a simple syntax to define the module stack for a given service.

Firstly, you must decide which service(s) you want your custom module to be integrated with. This could be a single service like SSH (sshd), or multiple services depending on your needs.

Once decided, you will need to edit the configuration file for each selected service. Let's use sshd as an example. Open the SSH PAM configuration file using a text editor with root privileges. You can use nano, vi, or any text editor of your choice. Next, add the following line to the configuration file:

auth required pam_python.so /usr/local/lib/security/my_module.py

Here's a breakdown of what this line means: * auth: This indicates the type of module. In this case, it's an authentication module, which verifies the user's identity. * required: This is the control flag. If the module returns a failure, the whole authentication process fails, and no subsequent modules of the same type (in this case, auth) are executed. * pam_python.so: This is the PAM module to be invoked. pampython.so runs Python scripts as PAM modules. * /usr/local/lib/security/my_module.py: This is the full path to the Python script we want pampython.so to execute.

Note that the line we added should be placed correctly according to the desired sequence of module execution in the stack.

Once you've saved and closed the file, PAM is configured to use your custom module for SSH authentication.

Remember, always back up your original configuration files before making any changes to them. In case something goes wrong, you can restore the original configuration. Be aware that incorrect PAM configurations can potentially lock you out of your system.

Configuring Module Parameters

You can pass parameters to your PAM module directly from the configuration file. In our custom module, we used parameters to configure the PIN file location and the maximum number of tries.

To set these parameters, you add them to the end of the line in the configuration file, like so:

auth required pam_python.so /usr/local/lib/security/my_module.py pin_file=/path/to/pin/file max_tries=3

In this line, pin_file is set to /path/to/pin/file, and max_tries is set to 3. These will be the default settings for the module unless they're overridden by the user.

This way, you can tailor the behavior of your module without modifying the Python code, making your module more flexible and reusable.

Testing

Testing is of paramount importance in any software development process, and it becomes especially critical when dealing with sensitive components like authentication modules. There are several reasons why rigorous testing should be an integral part of developing a custom PAM module:

Given these critical factors, it is clear that thorough testing is not just an optional step but an essential part of the development and integration of a custom PAM module. It contributes significantly to ensuring the stability, security, and reliability of the overall system.

Troubleshooting

When integrating and testing your PAM module, you might encounter some issues. Here's how to troubleshoot common problems, including which log files can provide useful insights.

If the module fails to authenticate despite providing valid credentials, check for any syntax or logical errors in your Python script. Also, verify if the PIN file is correctly formatted and contains the correct data. The Python error logs can often provide useful insights about what's causing the failure. Additionally, the /var/log/auth.log file records all authentication attempts, and checking this log can help you pinpoint the issue.

If it seems like your module isn't being invoked at all, double-check the PAM configuration. Make sure that the module is correctly referenced in the appropriate service's configuration file and the path to the Python script is accurate. If the problem persists, the /var/log/syslog or /var/log/secure (location depends on your system) can provide more information about system events and could contain details about why your module isn't being called.

If it appears that PAM isn't respecting the control flag (e.g., required, sufficient, etc.), ensure that the flag is correctly specified in the configuration file. Remember, the order of the modules in the configuration file matters, and they are processed in the order they appear. The /var/log/auth.log file can provide insights into the authentication process, which may help diagnose the problem.

If changes to the PAM configuration file don't seem to take effect, make sure you're editing the correct file for the service you're testing. Also, confirm that you've saved the changes and the configuration file has the correct permissions. The /var/log/syslog or /var/log/secure files may have relevant information if the system isn't recognizing your changes.

If you experience system-wide issues after deploying the module (e.g., users unable to log in), it might be because the module is incorrectly blocking authentications. Be very cautious when deploying a custom PAM module system-wide and always have a secure way to revert the changes. In such a case, both the /var/log/auth.log and the /var/log/syslog or /var/log/secure files can provide crucial information about what went wrong.

Remember, understanding PAM and its working is critical when troubleshooting a custom PAM module. Incorrect configurations can lead to system-wide issues, potentially locking all users out of the system. Always test your module thoroughly in a controlled environment before deploying it on a live system.

Conclusion

In this blog post, we explored Pluggable Authentication Module (PAM) and how to create a custom PAM module in Python. We learned about the importance of user authentication and how PAM provides a flexible framework for authentication.

By developing a custom PAM module, you can extend the authentication capabilities of your system. We discussed the structure and functionality of a Python-based PAM module and covered the integration and usage process.

Thorough testing is essential to ensure proper functionality and system stability. We highlighted the significance of troubleshooting and provided guidance on where to look for relevant log files.

Creating custom PAM modules allows you to tailor authentication processes to your specific needs, enhancing security and flexibility. We encourage you to continue exploring PAM and experimenting with custom modules to bolster the security of your systems.

Happy coding and secure authentication!