Linux PAM — How to create an authentication module

Avi Rzayev
9 min readOct 22, 2022

--

If you are familiar with Linux, you know that Linux offers good security. You can limit users and create groups with different privileges. I was researching Linux authentication, and I found that you have the power to decide how you authenticate users on Linux machines.

This tutorial will answer all your questions about PAM: What is it and How to use it?

What is PAM?

PAM(Pluggable Authentication Modules) is a collection of libraries that allows you to decide how you authenticate your users to different applications on your Linux OS.

The problem it solves

PAM is a mighty tool; it takes responsibility for the authentication from high-level applications.

Imagine that you are developing a new Linux package that needs to authenticate a Linux user before use.

You need to understand where Linux stores the users, how the passwords are hashed, and how to authenticate them. What if you connect your machine to LDAP? You will need to support authentication with LDAP users. What about superusers? Do they need to authenticate too?

When it comes to authenticating with a Linux user, it's not the best practice to implement this logic inside the applications.

PAM provides an API that authenticates Linux users (For example, using the login command).

Another usage of PAM is when we want to manage the authentication of users on our Linux machine. Let's say that we have an LDAP server that contains users, and we want that to be able to authenticate with all the typical applications. We need some way to tell our OS to accept those users too. In this scenario, you will write a module that will authenticate the user using an LDAP server.

PAM Structure

Abstract PAM Architecture
Abstract PAM Architecture

PAM is separated into four categories which are also called management groups. Each group manages different aspects of user authentication:

  • Account — Responsible for verifying the user. It checks if the user has the correct privileges to access a service.
  • Auth — Responsible for Authenticating user's credentials.
  • Session —Responsible for performing actions when the user session starts or ends. For example, create a home folder for the user if it does not exists.
  • Password — Responsible for changing the user's credentials when needed.

When creating a PAM module, you probably create a module that will match one of those management groups. The location of the modules is in /lib/security/

Structure of PAM files

The PAM configuration files is located in /etc/pam.d/

Content of /etc/pam.d/
Contents of /etc/pam.d/

You will see that there are many files. Each file holds a configuration for the authentication of a service.

Each file contains rules that define how PAM should authenticate the user:

type   control   module   arguments

PAM executes the rules in order and informs the application if the authentication is successful.

Type —It is the management group of the module. It defines when the module should be called.

Control — defines the behavior of PAM when the module succeeds or fails on authentication. Available flags:

  • Required: The module must succeed for the user to get authenticated. If a single module fails, still check the other modules.
  • Requisite: The module must succeed for the user to get authenticated. If the module fails, exit with failure and don't check the other modules.
  • Sufficient: This module can fail if there is at least another sufficient module that succeeds.
  • Optional: The result of the module is ignored and not considered when authenticating the user.

Module — It is the path of the module that will perform the authentication.

Arguments — Those are arguments that passed for the module.

The modules are Share Objects and located in /lib/security/

Example: Understanding common-auth

Let's see an example file to understand the flow of PAM fully.

common-auth is the file that handles the standard authentication of Linux users. Most of the other pam services configurations will include this file to enable authentication with local Linux users. The same applies to common-account, common-session, and common-password.

auth    [success=1 default=ignore]      pam_unix.so nullok
auth requisite pam_deny.so
auth required pam_permit.so
auth optional pam_cap.so

In the first line, the module pam_unix.so is called with an argument nullok when the user needs to authenticate with his credentials. We can write the control flags in other ways if we want to be more specific about handling the module's output; the flag [success=1 default=ignore] means the same as sufficient.

According to the Man page, pam_unix.so is a module for traditional password authentication. The argument nullok means that it accepts users with blank passwords.

pam_deny.so is checking if the previous modules succeeded or not. Because it is requisite, it means that if this module fails, the other modules won't run.

pam_premit.so is an empty module. It gives access to everyone without authentication. Theoretically, if we delete the first two lines, users won't need to authenticate with applications.

pam_cap.so sets the current process' inheritable capabilities. However, because it is optional, it does not have any affection on the authentication process.

Creating a PAM module using Python

Now that you have a basic understanding of a PAM, let's create a PAM module that will funnily authenticate users: When our user needs to authenticate with credentials, we will give him a minigame to play instead. If he wins — he gets authenticated. If he loses — his action is denied.

We will use an existing PAM module made by Russell Stuart, which allows the creation of PAM modules with Python. It is called pam_python.

Installation

You can download pam_python using this link: https://sourceforge.net/projects/pam-python/files/.

Extract the files and run make to install the module. After the installation, you will find the pam_python.so module inside /lib/security/

Documentation

You can find the documentation of pam_python at this link: https://pam-python.sourceforge.net/doc/html/.

It explains the API of pam that you should provide in your script.

Main Script

My project includes multiple python files, so I decided not to have them all to keep the simplicity. You can find the complete project here: https://gitlab.com/avirzayev/pam-minigames.

The main script is login.py, which will be the PAM module's entry point. It must provide all the API methods for PAM to work.

import sitesite.main()import random
import pathlib
from pam_minigames.minigames import math_game, palindrome_game, time_game
from pam_minigames.utils.utils import is_user_allowed
from pam_minigames import colors
from pam_minigames.utils.print_utils import print_section_header, print_section
games_list = [math_game, palindrome_game, time_game]def get_user(pamh):
try:
user = pamh.get_user(None)
return user
except pamh.exception as e:
return e.pam_result
def pam_sm_authenticate(pamh, flags, argv):
user = get_user(pamh)
if not is_user_allowed(user):
return pamh.PAM_USER_UNKNOWN
game = random.choice(games_list) if game():
print_section_header("AUTHENTICATED!")
return pamh.PAM_SUCCESS
else:
print_section_header("UNAUTHENTICATED!", header_color=colors.RED)
return pamh.PAM_AUTH_ERRdef pam_sm_open_session(pamh, flags, argv):
user = get_user(pamh)
if not is_user_allowed(user):
return pamh.PAM_USER_UNKNOWN
home_dir = pathlib.Path("/home/" + user) if not home_dir.exists():
print_section(text="Hmmm... someone here doesnt have a home directory...\n"
"Don't worry, I am on it!",
header_text="Pam Message!", header_color=colors.PURPLE)
home_dir.mkdir() return pamh.PAM_SUCCESSdef pam_sm_close_session(pamh, flags, argv):
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_chauthtok(pamh, flags, argv):
return pamh.PAM_SUCCESS

The function pam_sm_authenticate is in the auth management group. When the user is requested for credentials — this function will run. The parameter pamh is the PAM object which gives information about the login session. You can get the username using that parameter — it's done with the get_user function to prevent code duplication.

In this line:

if not is_user_allowed(user):
return pamh.PAM_USER_UNKNOWN

I am limiting access only to allowed users. I don't want our module to authenticate the root user. I still want to keep the security and still be able to work on my machine without any disruptions. I am checking if the user is in a group named fungames that I created using groupadd. The module will authenticate only users within that group.

game = random.choice(games_list)if game():
print_section_header("AUTHENTICATED!")
return pamh.PAM_SUCCESS
else:
print_section_header("UNAUTHENTICATED!", header_color=colors.RED)
return pamh.PAM_AUTH_ERR

Later I have a list of premade games. When the user wins the game, the function will return the PAM_SUCCESS code, which means the user is authenticated. Otherwise, it will return the PAM_AUTH_ERR code, which means the user is not authenticated.

def pam_sm_open_session(pamh, flags, argv):
user = get_user(pamh)
if not is_user_allowed(user):
return pamh.PAM_USER_UNKNOWN
home_dir = pathlib.Path("/home/" + user) if not home_dir.exists():
print_section(text="Hmmm... someone here doesnt have a home directory...\n"
"Don't worry, I am on it!",
header_text="Pam Message!", header_color=colors.PURPLE)
home_dir.mkdir() return pamh.PAM_SUCCESS

Besides the authentication, I also implemented the session management group. So when the user logs in and the module detects that the user does not have a home directory, it will create it on its own and notify the user.

Adding the module to /etc/pam.d

Remember where we need to configure the PAM to use a new module? It's in /etc/pam.d

We want that any action that requires credentials will trigger our module. So we will add rules to common-auth and common-session files to achieve that because we only implemented those groups.

The new common-auth file will look like this:


auth sufficient pam_python.so /opt/pam_minigames/login.py
auth [success=1 default=ignore] pam_unix.so nullok
auth required pam_permit.so
auth optional pam_cap.so

The newly added rule is in the first line:

auth    sufficient                      pam_python.so /opt/pam_minigames/login.py

The rule says that when the user needs to authenticate, call the pam_python.so module and pass him the path of the script that should be running.

The same applies to common-session:

session sufficient                      pam_python.so /opt/pam_minigames/login.py
session [default=1] pam_permit.so
session requisite pam_deny.so
session required pam_permit.so
session optional pam_umask.so
session required pam_unix.so
session optional pam_systemd.so

Notice that the control on both files is sufficient. We don't want our module to be the only way for authentication. We expect that the authentication fails if the user is not inside the fungames group. So we need to keep the old authentication to let the other users authenticate with other methods.

Now that we added everything, our module should work!

Testing the new PAM module

I have created a test user name, cooluser, and added it to the fungames group. You can achieve that by using useradd and groupadd commands.

Let's see what happens when I switch a user:

Switching a user

Let's see now when I switch back to my user that is not in the fungames group:

Switching back to the original user

Let's see a different minigame:

Another minigame

Also when using the sudo command triggers the module:

Using sudo

But what happens when we lose the game?

Losing a game

Notice that when the authentication with our module fails, it tries to authenticate with the next available module, which is pam_unix.so

The sufficient keyword is not the proper control for that use case. We want to authenticate fungames users with our module and skip the pam_unix.so module. But we still wish for users not in the fungames group to manage authenticating with pam_unix.so.

We can achieve that by reading the return code of our module and creating a complicated control that acts differently with different return codes. You can find its syntax here.

Remember those lines?

if not is_user_allowed(user):
return pamh.PAM_USER_UNKNOWN
...
...
return pamh.PAM_AUTH_ERR

We are returning different return codes for different situations. So we can use that and change our control to act like requisite if it gets PAM_AUTH_ERR and act like sufficient when it gets PAM_USER_UNKNOWN.

Our new lines will look like this:

auth    [user_unknown=ignore auth_err=die success=done]                     pam_python.so /opt/pam_minigames/login.pysession sufficient                     pam_python.so /opt/pam_minigames/login.py

Now when we lose our game:

Fixed PAM control

The regular user authentication didn't get affected:

A typical user doesn't get affected

Conclusion

We learned the purpose of PAM and how to implement it. We covered the structure of PAM, management groups, and control flags. We also made a little demo with Python.

PAM is a potent tool, and you can do amazing and unique projects that include PAM.

Happy coding, and try not to get locked out of your system. I got locked out from my machine many times while working on this project :)

--

--