In this guide, we’ll teach you how to create your own PAM module. The difference here is that we’ll be using Golang (instead of the traditional C language). We will also make use of CGO, which is a requirement for most of the system calls in a PAM, as it's written in C.
Please note that I have tested this guide on Ubuntu and all commands are Debian based.
Welcome to my complete guide on creating your own Pluggable Authentication Module using Golang. If you’re new to programming or system administration, don’t worry! This manual will take you through all the steps you need to know in order to get up and running with your very own PAM. Golang is an increasingly popular language for developers due to its high performance and scalability. It’s perfect for large enterprise applications or small projects alike.
The goal here is quite simple: create a CGO-based PAM module in Golang. By doing so, we can communicate directly with the C code required to interact with the PAM API. Whether you’re trying to improve security on a Linux server or build something from scratch, understanding how PAM modules work will push your project further than ever before.
There are already pre-existing golang based PAM modules out there, but none of them serve my purpose for developing a PAM module with a unique authentication method.
Important PAM stack functions
Developing a PAM module means knowing the core PAM stack functions. These building blocks allow you to interact with the PAM system and manage authentication. These are few of those functions:
- Authentication:
pam_authenticate
: Verifies user identity by checking the username and password.pam_setcred
: Establishes user’s credentials after successful authentication, or it can refresh or delete them.
- Session Management:
pam_open_session
: Starts new session for authenticated user oncepam_setcred
has been called.pam_close_session
: Shuts down current session. It should clean up anything initiated bypam_open_session
.
- Account Management:
pam_acct_mgmt:
Checks if the user's account is valid (e.g., not expired, has access permissions, etc.)
- Password Management:
pam_chauthtok
: Changes user’s authentication token (usually password). Usually uses old password and new password as parameters.
- Environment Handling:
pam_getenv:
Retrieves environment variables for the PAM session.pam_putenv:
Sets environment variables for the PAM session.pam_getenvlist
: Retrieves a list of all PAM environment variables.
- Utility Functions:
pam_get_item
,pam_set_item
: Manages PAM items.pam_strerror
: Translates error code to string
- Conversation Struct:
pam_conv
: Handles conversation between application and user
- Custom or Wrapper Functions:
get_user
: A custom function to retrieve username usually a wrapper forpam_get_item
Why we need CGO to develop PAM Module instead of GO?
Constructing a Pluggable Authentication Module (PAM) isn't a simple task. It often calls for low-level access that just isn't available in Go by default. The language is safe, simple, and efficient, but it doesn’t offer the same direct access to system capabilities as C does. That’s where CGO comes into play.
- C Library Interface: PAM is primarily composed of shared libraries written in C. To interface with those C libraries directly, CGO can be incredibly useful.
- System-Level Access: For authentication and session management—two key steps in creating any PAM module—CGO lets you interact with your OS directly.
- Code Reuse: No need to reinvent the wheel if you have existing C-based PAM modules or systems. Just extend or adapt them without reimplementation thanks to inter-language operability via CGO.
- Functionality Gap: If there's a specific system functionality not natively supported by Go, there's a good chance you can use CGO to bridge that gap.
- Performance: Performance-critical operations are best left to optimized C libraries. And because CGO allows using them in Go codebases, those operations can still be handled seamlessly.
- Type Matching: Complex C data types pose no challenge when they need to be used in Go code through PAM APIs. You guessed it — that’s another job for CGO and its conversion mechanisms.
- Legacy Support: Using CGO means your new project won’t abandon any of your previous work. Whether it’s an old module or an entire system built on top of C-based legacy PAM modules, integration remains possible.
Having said that, we use CGO only to an extent where we need to use the native C functions while the rest of the code will be written purely in GO.
Steps to create PAM Module using CGO and GO
I have already written a detailed article on Getting started with CGO Programming Language. Here I will try to be very brief about this topic.
For our article we will create two files for the PAM Module:
- auth.go : This contains the code to actually authenticate the user
- pam.go: This contains C functions to fetch user and password information
1. Install GO, GCC and other required packages
This goes without saying, you must have GO binary available or installed.
sudo apt update sudo apt install golang-go
Install the required compilers
sudo apt update sudo apt install build-essential
Verify the GO installation
# go version go version go1.18.1 linux/amd64
Verify GCC Installation
# gcc --version gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0
sudo apt update sudo apt install git
Verify Git Installation
# git --version git version 2.25.1
PAM Development Libraries
sudo apt update sudo apt install libpam0g-dev OR dnf install pam-devel # On RHEL Based Distributions
2. Setup Projects Directory
You will need to setup your projects directory where you will be developing your PAM module. I am going to do this complete exercise under ~/projects/pam
:
mkdir -p ~/projects/pam cd ~/projects/pam
3. Define PAM Functions in CGO to Fetch Username and Password (pam.go)
In this section we will create our pam.go
which will basically get the user and password information from the interface used to perform the authentication such as ssh, su etc.
The first lines in the code snippet serve as preprocessor directives:
/*
#cgo LDFLAGS: -lpam
#include <security/pam_ext.h>
#include <security/pam_modules.h>
Here, the #cgo LDFLAGS: -lpam
instruction specifies that the PAM library should be linked during the compilation stage. The inclusion of the headers pam_appl.h
and pam_modules.h
enables the use of PAM functionalities necessary for building the module.
The following line of code, #cgo LDFLAGS: -lpam
, tells the program that when it’s time to compile everything together, it should also link the PAM library. Then we have two Go include statements which imports the headers for pam_appl.h
and pam_modules.h
.
This can vary based on your Linux flavor, On RHEL based distros this will be as shown below:
/*
#cgo LDFLAGS: -lpam
#include <security/pam_appl.h>
#include <security/pam_modules.h>
It basically depends on the fact that which module contains pam_get_authtok
function. This can be checked using:
# grep -r "pam_get_authtok" /usr/include/security/ /usr/include/security/_pam_types.h:#define PAM_AUTHTOK_TYPE 13 /* The type for pam_get_authtok */ /usr/include/security/pam_ext.h:pam_get_authtok (pam_handle_t *pamh, int item, const char **authtok, /usr/include/security/pam_ext.h:pam_get_authtok_noverify (pam_handle_t *pamh, const char **authtok, /usr/include/security/pam_ext.h:pam_get_authtok_verify (pam_handle_t *pamh, const char **authtok,
Based on the highlighted module which contains pam_handle
, you can update the directives.
Next is:
extern int go_authenticate(pam_handle_t *pamh);
This is a forward declaration of a function written in Go. It means that we can call the Go function go_authenticate
from C code. The Go function will then call back into PAM (using the cgo bridges) to fetch data like the username and password.
const char* c_username;
const char* c_password;
c_username and c_password:
Global variables to store the username and password (or authentication token).
int get_authtok(pam_handle_t* pamh) {
return pam_get_authtok(pamh, PAM_AUTHTOK, &c_password , NULL);
}
pam_handle_t* pamh
: A handle for the current PAM transaction.pam_get_authtok()
: This is a PAM API function that gets an authentication token. For most systems this should beget_password()
. Whatever this returns will be stored inc_password
in our code.
int get_user(pam_handle_t* pamh) {
return pam_get_user(pamh, &c_username, "Username: ");
}
pam_get_user()
: A PAM API function to get the username. The username is stored in c_username
.
int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return go_authenticate(pamh);
}
pam_sm_authenticate
: The main function that will be called for authentication.go_authenticate(pamh)
: Apparently calls a Go function for actual authentication (this function is not provided in the code).
int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return PAM_SUCCESS;
}
pam_sm_setcred
: A PAM function for setting credentials. In this case, it returns PAM_SUCCESS right away so It’s a no-op.
The additional Go code is the Go bindings to the C code above. It wraps the C functions so that they can be called more idiomatically in Go.
func GetUser(logger *logrus.Logger, pamh *C.pam_handle_t) (string, error) {
ret := C.get_user(pamh)
if ret != C.PAM_SUCCESS {
logger.Errorln("Username could not be retrieved")
return "", errors.New("username could not be retrieved")
}
return C.GoString(C.c_username), nil
}
func GetUser(logger *logrus.Logger, pamh *C.pam_handle_t) (string, error)
: This defines a Go functionGetUser
which takes alogrus.Logger
for logging and apam_handle_t
pointer from C.ret := C.get_user(pamh)
: Call theget_user
C function and store its return value intoret
.if ret != C.PAM_SUCCESS { ... }
: Check if the call succeeded, if not then log an error and return an error object. Checks if the function succeeded.return C.GoString(C.c_username), nil
: Convert the c string to go string and return that with anil
error.
func GetPassword(logger *logrus.Logger, pamh *C.pam_handle_t) (string, error) {
ret := C.get_authtok(pamh)
if ret != C.PAM_SUCCESS {
logger.Errorln("User password could not be retrieved")
return "", errors.New("user password could not be retrieved")
}
return C.GoString(C.c_password), nil
}
func GetPassword(logger *logrus.Logger, pamh *C.pam_handle_t) (string, error)
: This is another function that's pretty much the same asGetUser
, except it gets the password you're checking or authentication token you want.ret := C.get_authtok(pamh)
:get_authtok
is called and its return value is stored inret
.if ret != C.PAM_SUCCESS { ... }
: does the exact same thing asGetUser
.return C.GoString(C.c_password), nil
: The secret to the password is converted to a go string and returned. It'll be up to you to make sure this information isn't mishandled since the password can be seen by any user who calls this function and runs your code.
Here is the complete code of pam.go
for reference:
package main
/*
#cgo LDFLAGS: -lpam
#include <security/pam_ext.h>
#include <security/pam_modules.h>
extern int go_authenticate(pam_handle_t *pamh);
const char* c_username;
const char* c_password;
// Function to get the username from PAM.
int get_authtok(pam_handle_t* pamh) {
return pam_get_authtok(pamh, PAM_AUTHTOK, &c_password , NULL);
}
// Function to get the password (or authentication token) from PAM.
int get_user(pam_handle_t* pamh) {
return pam_get_user(pamh, &c_username, "Username: ");
}
int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return go_authenticate(pamh);
}
int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return PAM_SUCCESS;
}
*/
import "C"
import (
"errors"
"github.com/sirupsen/logrus"
)
func GetUser(logger *logrus.Logger, pamh *C.pam_handle_t) (string, error) {
ret := C.get_user(pamh)
if ret != C.PAM_SUCCESS {
logger.Errorln("Username could not be retrieved")
return "", errors.New("username could not be retrieved")
}
return C.GoString(C.c_username), nil
}
func GetPassword(logger *logrus.Logger, pamh *C.pam_handle_t) (string, error) {
ret := C.get_authtok(pamh)
if ret != C.PAM_SUCCESS {
logger.Errorln("User password could not be retrieved")
return "", errors.New("user password could not be retrieved")
}
return C.GoString(C.c_password), nil
}
4. Define PAM Function to perform authentication (auth.go)
Now we have ways of getting the username and password from whoever's trying to authenticate. Next we need a way of actually authenticating them. In our case we use keycloak for authentication. Look at its logo! It’s just so cool!
Here's an outline for a function that will allow you to build your own authenticate function:
package main
/*
#cgo LDFLAGS: -lpam
#include <security/pam_appl.h>
#include <security/pam_modules.h>
*/
import "C"
//export go_authenticate
func go_authenticate(pamh *C.pam_handle_t) C.int {
// Fetch username and password from PAM
// Assume GetUser and GetPassword are defined elsewhere
username, err := GetUser(nil, pamh)
if err != nil {
// Log the error and return PAM authentication failure
return C.PAM_AUTH_ERR
}
password, err := GetPassword(nil, pamh)
if err != nil {
// Log the error and return PAM authentication failure
return C.PAM_AUTH_ERR
}
// Add your logic to authenticate the user
}
Here is a sample code which can be used to perform an authentication attempt using keycloak with client credentials:
package main
/*
#cgo LDFLAGS: -lpam
#include <security/pam_appl.h>
#include <security/pam_modules.h>
*/
import "C"
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
// Constants for Keycloak credentials and endpoint
const (
keycloakTokenURL = "https://keycloak.example.com/auth/realms/my-realm/protocol/openid-connect/token"
clientID = "my-client-id"
clientSecret = "my-client-secret"
)
//export go_authenticate
func go_authenticate(pamh *C.pam_handle_t) C.int {
// Fetch username and password from PAM
// Assume GetUser and GetPassword are defined elsewhere
username, err := GetUser(nil, pamh)
if err != nil {
// Log the error and return PAM authentication failure
return C.PAM_AUTH_ERR
}
password, err := GetPassword(nil, pamh)
if err != nil {
// Log the error and return PAM authentication failure
return C.PAM_AUTH_ERR
}
// Create a context with a timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Prepare form data
form := url.Values{}
form.Set("grant_type", "password")
form.Set("client_id", clientID)
form.Set("client_secret", clientSecret)
form.Set("username", username)
form.Set("password", password)
// Create the HTTP request
req, err := http.NewRequestWithContext(ctx, "POST", keycloakTokenURL, strings.NewReader(form.Encode()))
if err != nil {
// Log the error and return PAM authentication failure
return C.PAM_AUTH_ERR
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
// Log the error and return PAM authentication failure
return C.PAM_AUTH_ERR
}
defer resp.Body.Close()
// Read the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// Log the error and return PAM authentication failure
return C.PAM_AUTH_ERR
}
// Check for successful status
if resp.StatusCode != http.StatusOK {
// Log the error and return PAM authentication failure
fmt.Println("Authentication failed with status:", resp.Status)
fmt.Println("Body:", string(body))
return C.PAM_AUTH_ERR
}
// Further validation can be done here, like checking the token if needed
// If everything went well
return C.PAM_SUCCESS
}
The go_authenticate function links PAM (Pluggable Authentication Module) with Keycloak for user authentication by being callable from C code. This is what it does:
- Retrieves the username and password from PAM.
- Sets up an HTTP POST request with everything needed in order for Keycloak to know if these credentials are valid or not
- Sends off the request then waits patiently until Keycloak replies back
- If the status code is OK (200), the function returns a PAM success status; otherwise, it returns a PAM authentication error.
The function uses direct HTTP calls to accomplish OAuth 2.0 password grant-type authentication against a Keycloak server.
5. Initialize Project
Next initialize the pam project to create go.mod and go.sum
file which will download all the required dependencies:
cd ~/projects/pam go mod init pam go mod tidy
This should import and get all the third party modules used in your go code. In our case we have only use logrus for the logging purpose.
6. Compile and Build PAM Module
To build the shared library that integrates with PAM, you can use the following command:
CGO_CFLAGS="-g -O2" go build --buildmode=c-shared -o /tmp/keycloak_pam.so auth.go pam.go
This will generate our PAM Module:
# ls -l /tmp/keycloak_pam.so -rw-r--r-- 1 root root 6881576 Sep 21 11:03 /tmp/keycloak_pam.so
7. Adding custom PAM module to system library
To load your keycloak_pam.so
module, we will want to place it in the same directory with all of the other PAM modules.
- On Ubuntu based systems, you’d usually install it in
/usr/lib/x86_64-linux-gnu/security/
. - On RHEL-based systems, you’ll install it in
/usr/lib64/security/
.
But this might change depending on many things like your system architecture and distribution.
One way to find out where they are on yours is to see where existing modules are using the find command:
find /lib* /usr/lib* -type d -name security
Next, try looking for other pam modules such as pam_unix.so
and put your keycloak_pam.so
there with execute permissions
cp /tmp/keycloak_pam.so /usr/lib/x86_64-linux-gnu/security/ chmod +x /usr/lib/x86_64-linux-gnu/security/keycloak_pam.so
8. Update PAM Configuration Files
PAM (Pluggable Authentication Modules) uses configuration files that control how authentication is done for programs that use PAM. These are usually kept in a directory called /etc/pam.d/
on Linux systems. Each program that uses PAM will have its own file in that directory.
A pam configuration file specifies how authentication should be handled for that program and consists of multiple lines, each line representing one rule. A basic rule has the following format:
<module-type> <control-flag> <module-path> <module-arguments>
Explanation:
<module-type>
: This field identifies what type of job this module does. This can be:auth
: Authenticating usersaccount
: Checking account requirements such as expiration etcpassword
: Changing passwordssession
: Setting up or tearing down sessions
<control-flag>
: It’s used to specify how failures should be treated.required
: This module must succeed in order for the stack to continue trying other rules.requisite
: This module must succeed immediately or else no further processing shall happen.sufficient
: If this module succeeds and no previousrequired
modules have failed, the user is authenticated.optional
: It’s only useful if there are also other modules being called.
<module-path>
: The path to the PAM module. For system modules, you can use the module's name without a path (e.g.,pam_unix.so
). For custom modules, you'll typically provide the full path (e.g.,/path/to/keycloak_pam.so
).<module-arguments>
: Any arguments the module needs.
Now comes the part where you’ll have to choose which programs you want to secure with your module. Here’s a list of some common programs and their pam configuration files:
Service | PAM Configuration File | Description |
---|---|---|
SSH | sshd |
Handles authentication for Secure Shell daemon (SSH). |
Switch User (SU) | su |
Manages policies for the su command to change to another user. |
Login | login |
Manages local system logins from terminals or non-SSH remote connections. |
Password Management | passwd |
Used when changing passwords with the passwd command. |
Sudo | sudo , sudoers |
For the sudo command which lets permitted users execute commands as another user. |
Cron | cron |
Manages authentication for scheduled cron jobs. |
Graphical Login | gdm-password , lightdm |
Used for graphical logins. The specific file may vary depending on the display manager used (GDM, LightDM, etc.). |
Remote File Systems | mount |
Manages authentication for mounting Remote File Systems. |
User Sessions | system-auth , password-auth |
Central configurations for general authentication, account, and session rules (especially important on RHEL/CentOS). |
API/Auth requests | Varies | The exact file would depend on the services or daemons being used. Some might have custom PAM configurations. |
Edit the chosen PAM configuration file. You can use editors like vi
or nano
. Add a line for your module, such as:
auth requisite keycloak_pam.so
pam_unix.so
because system users have to be authenticated with the default PAM module and such requests should not reach keycloak_pam.so
as this will only be used for Keycloak based user.
9. Testing PAM Module
After creating and deploying your custom PAM module, testing it is essential to make sure it works as expected and does not introduce security vulnerabilities inadvertently.
1. Using Different PAM Reliant Services
Different services use PAM for authentication. Test your module with each service that you expect it to integrate with.
- SSH:
- Restart SSH service after updating its related PAM configuration file.
- Try SSHing into the machine. Observe behavior and ensure that your module authenticates as expected.
- Login:
- If you've integrated with the system's primary login, try to log in from the login screen.
- Make sure you keep a session active or have other means of accessing the system if your module doesn't work as expected. You don't want to lock yourself out.
- su (switch user):
- From a terminal, use the
su
command to switch users. - Make sure your module behaves as expected especially if trying to switch to superuser account
- From a terminal, use the
2. Debugging and Logging
To effectively test and troubleshoot, you need visibility into what your module is doing.
- Logging:
- Use logging libraries or functions to log key events in your module. For example, in Golang, you can use standard log package or logrus module.
- Make sure you log successful events and errors but be careful about logging sensitive information like passwords
- PAM's Debugging:
- Some PAM modules or configurations support debug mode which can provide more verbose logs helping pin point issues
- Check logs usually at
/var/log/auth.log
(on Ubuntu) or/var/log/secure
(on CentOS)
3. Common Errors and Troubleshooting
When working on PAM especially custom modules, you may run into various issues. Here are some common problems and solutions:
- Permission Issues: Make sure your
.so
file has correct permissions. Usually it should be readable by root. - Module Not Found: Make sure you put the module in correct directory and specified right path in PAM configuration
- Authentication Failures:
- If a authentication fails unexpectedly, make sure your module interfaces with authentication service correctly (example Keycloak in our case)
- Check if credentials are correctly passed to the module
- Module Crashes:
- Check for any potential null pointer dereferences or array bounds issues in your code
- Ensure proper error handling especially for unexpected situations.
- Lockouts:
- If you lock yourself out with your module, having a backup access method (another SSH key or session) can save you
- As a preventive measure before extensively testing on production system, always try your module on test machine or VM.
Key Takeaways
All throughout this journey, we've given ourselves to Pluggable Authentication Modules (PAM) and how it fits in with Linux-based systems. We began by understanding the core principles and then moved on to building our own plug-in module using Golang.
Here are some things we learned in this journey:
- Introduction to PAM: This was where we got a feel of what PAM is all about; I’m talking about how it modularizes authentication mechanisms for different services on Linux. It also allows flexibility and integration with several authentication providers.
- Building a Custom Module: With Golang as our weapon of choice, you can bet that things were going to get interesting. And they did!
- Deployment: After cooking up our module, the next step was deploying it in the right directories. We went ahead to integrate it with various services such as su, ssh etc by modifying their respective PAM configuration files.
- Testing: To make sure everything worked as intended, there was extensive testing done at every stage of development. This helped us confirm if our plug-in behaved well across different services or not.
great article, thanks a lot!