Table of Contents
Welcome to this comprehensive guide on creating a Pluggable Authentication Module (PAM) using Golang, one of the most popular programming languages for building scalable and high-performance applications. This tutorial aims to serve as a step-by-step manual for developers and system administrators looking to harness the power of Golang in conjunction with PAM for robust and flexible authentication mechanisms.
The objective of this guide is to walk you through the process of writing a CGO-based PAM module in Golang. CGO, or C-Go, is a Go package that provides a way to call C code from Go, and it plays a pivotal role in interacting with the PAM API. Whether you're looking to secure a Linux server or extend authentication capabilities for an enterprise application, understanding how to create a PAM module using Golang will offer you a highly flexible and customizable authentication strategy.
There is already golang based PAM modules available https://pkg.go.dev/github.com/msteinert/pam but this didn't serve my purpose wherein I can develop a PAM module with my own way of authentication
Important PAM stack functions
When developing a Pluggable Authentication Module (PAM), understanding the core PAM stack functions is essential. These functions are the building blocks that help you to interact with the PAM system and manage the authentication process. Here's a list of some key PAM stack functions that are generally important when developing a PAM module:
- Authentication:
pam_authenticate
: Validates user identity. Typically checks username and password.pam_setcred
: Sets the user's credentials after successful authentication. Can also refresh or delete credentials.
- Session Management:
pam_open_session
: Initiates a new session for the authenticated user, usually afterpam_setcred
has been called.pam_close_session
: Closes the existing session. It should clean up anything started 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 the user's authentication token (usually the password). This function often incorporates the current (old) password and the new password.
- 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
: Error code to string translation.
- Conversation Struct:
pam_conv
: Handles conversation between the application and the user.
- Custom or Wrapper Functions:
get_user
: A custom function to retrieve the username, usually a wrapper forpam_get_item
.
Why we need CGO to develop PAM Module instead of GO?
Creating a Pluggable Authentication Module (PAM) often requires low-level access to system resources and APIs, which are usually provided through C libraries. While Go is an incredibly powerful and flexible language, it is designed with a different set of priorities compared to C. Go prioritizes safety, simplicity, and efficiency but does not provide direct access to low-level system capabilities in the same way C does. This is where CGO comes in when you're developing a PAM module.
- C Library Interface: PAM relies on shared libraries primarily written in C; CGO allows Go to interface directly with these C libraries.
- System-Level Access: Direct OS interactions needed for authentication and session management are facilitated by CGO.
- Code Reuse: Existing C-based PAM modules can be extended or adapted without reimplementation, leveraging CGO for inter-language operability.
- Functionality Gap: Certain low-level system functionalities not natively supported by Go can be accessed through CGO.
- Performance: CGO allows utilization of optimized C libraries for performance-critical operations.
- Type Matching: Complex C data types used in PAM APIs can be handled in Go through CGO's type conversion mechanisms.
- Legacy Support: Integration with existing C-based legacy PAM modules and systems is made possible via CGO.
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.
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.
CGO is a technology that allows Go programs to interoperate with C libraries and codebases. It serves as a bridge between the Go and C programming languages, enabling Go developers to call C functions, utilize C data structures, and even incorporate C code directly into their Go applications.
For our article we have two files
- auth.go : This contains the code to actually authenticate the user
- pam.go: This contains C functions to fetch user and password information
1. Pre-requisites
Before we start, you must need to setup your environment. This article is tested using Ubuntu so the commands are very much specific to Debian based environment:
Install GO
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
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
2. 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.
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:
On Ubuntu:
# 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,
On CentOS:
# grep -r "pam_get_authtok" /usr/include/security/ /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, /usr/include/security/_pam_types.h:#define PAM_AUTHTOK_TYPE 13 /* The type for pam_get_authtok */
Next is:
extern int go_authenticate(pam_handle_t *pamh);
This is a forward declaration of a function written in Go. By doing this, you can call the Go function go_authenticate
from C code. The Go function itself will then call back into PAM (using the cgo bridges) to fetch necessary 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()
: A PAM API function to get the authentication token (usually the password). The token is stored inc_password
.
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 related to setting credentials. Here, it returns PAM_SUCCESS
immediately, meaning it's a no-op (no operation).
The additional Go code here acts as the Go bindings to the C code written earlier. It essentially wraps the C functions so that they can be called in a more idiomatic Go way.
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)
: Defines a Go functionGetUser
that takes alogrus.Logger
for logging and apam_handle_t
pointer from C.ret := C.get_user(pamh)
: Calls theget_user
C function and stores the return value inret
.if ret != C.PAM_SUCCESS { ... }
: Checks if the function succeeded. If not, it logs an error and returns an error object.return C.GoString(C.c_username), nil
: Converts the C string to a Go string and returns it, along 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)
: Similar toGetUser
, but this function retrieves the password or authentication token.ret := C.get_authtok(pamh)
: Calls theget_authtok
C function and stores the return value inret
.if ret != C.PAM_SUCCESS { ... }
: Again, checks if the function succeeded and returns an error if not.return C.GoString(C.c_password), nil
: Converts the C string containing the password to a Go string and returns it.
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
}
3. Implement authentication in PAM Module (auth.go)
Now we have a way to get the username and password of a user so next we need a way to authenticate these users. In our case we are using keycloak for the authentication.
Here is a high level template of the authenticate function where you can add your logic and attempt the authentication:
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:
NOTE: This is not an actual code but just to give you an example of how an authentication function can be written. Here go_authenticate
is the main function where you will add your logic to return C.PAM_SUCCESS
for successful authentication and C.PAM_AUTH_ERR
for failed authentication.
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 serves as a bridge between PAM (Pluggable Authentication Module) and Keycloak for user authentication. It's meant to be called from C code. It does the following:
- Retrieves the username and password from PAM.
- Sets up an HTTP POST request to the Keycloak token endpoint, including the username, password, client ID, and client secret in the form data.
- Sends the request and checks for a successful HTTP status code from Keycloak.
- 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.
Next initialize the pam project:
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.
4. 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
Adding your custom PAM module
To use your keycloak_pam.so
module, we will need to place it inside the same location where all other PAM modules are added.
- On Ubuntu based distribution, you'd typically place it in
/usr/lib/x86_64-linux-gnu/security/
. - On RHEL based distribution, you'd place it in
/usr/lib64/security/
.
But again this may vary on other factors such as your system architecture, distributions etc.
One way to find out where the PAM modules are located on your system is to look where existing modules reside:
find /lib* /usr/lib* -type d -name security
Next you can try to search other PAM modules such as pam_unix.so and place your keycloak_pam.so in the same directory with executable permission
cp /tmp/keycloak_pam.so /usr/lib/x86_64-linux-gnu/security/ chmod +x /usr/lib/x86_64-linux-gnu/security/keycloak_pam.so
Update PAM Configuration Files
PAM (Pluggable Authentication Modules) uses configuration files to control the authentication methods for each service that relies on PAM. These files are generally located in /etc/pam.d/
directory on Linux systems. Each service that utilizes PAM will have its own configuration file in this directory.
A PAM configuration file dictates how authentication should be handled for that service and is made up of multiple lines, each representing a PAM rule. A basic rule has the following format:
<module-type> <control-flag> <module-path> <module-arguments>
Explanation:
<module-type>
: Indicates the module's purpose. Common types include:auth
: Handle authentication tasks.account
: Verify that access is allowed (e.g., checking account expiration).password
: Handle password-related tasks.session
: Handle tasks related to setting up and tearing down sessions.
<control-flag>
: Dictates how failures are handled. Common flags include:required
: The module must succeed for authentication to continue. If it fails, the user is notified after the rest of the stack is checked.requisite
: If this module fails, the user is notified immediately.sufficient
: If this module succeeds and no previousrequired
modules have failed, the user is authenticated.optional
: Only useful with other modules.
<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.
Next choose which services you want to secure with your module. Here's a list of some common services and their associated 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
NOTE: You must test and determine the position to place this entry in the PAM configuration file. It should be in proper order to make sure the existing authentication flow is not impacted. In our case we are adding the entry after pam_unix.so to make sure the system users are authenticated with default PAM module and such requests do not reach keycloak_pam.so as this is to be used only for keycloak based user.
Testing PAM Module
After creating and deploying our custom PAM module, testing is essential to ensure that it works as expected and does not inadvertently introduce security vulnerabilities.
1. Using Different PAM Reliant Services
Different services use PAM for authentication. It's vital to test your module with each service you expect it to integrate with.
- SSH:
- Restart the SSH service after updating its related PAM configuration file.
- Attempt to SSH into the machine. Observe the behavior and ensure that your module authenticates as expected.
- Login:
- If you've integrated with the system's primary login, attempt to log in from the login screen.
- Ensure to keep a session active or have other means of accessing the system in case the 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. - Ensure your module behaves as expected, especially if trying to switch to a 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 the standard
log
package. - Make sure to log both successful events and errors, but be cautious about logging sensitive information such as passwords.
- Use logging libraries or functions to log key events in your module. For example, in Golang, you can use the standard
- PAM's Debugging:
- Some PAM modules or configurations support a debug mode. If available, this can provide more verbose logging, helping pinpoint issues.
- Check the logs, usually in
/var/log/auth.log
(on Ubuntu) or/var/log/secure
(on CentOS).
3. Common Errors and Troubleshooting
When working with PAM and especially custom modules, you may encounter various issues. Here are some common problems and solutions:
- Permission Issues: Ensure your
.so
file has the correct permissions. Usually, it should be readable by root. - Module Not Found: Ensure you've placed the module in the correct directory and specified the right path in the PAM configuration.
- Authentication Failures:
- If authentication fails unexpectedly, ensure that your module is correctly interfacing with the authentication service (e.g., Keycloak in your case).
- Check if the credentials are correctly passed to your 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 find yourself locked out due to your module, having a backup access method (like another SSH key or session) can be a lifesaver.
- As a preventive measure, before extensively testing on a production system, always try your module on a test machine or VM.
Conclusion
Throughout this journey, we've taken a deep dive into the world of Pluggable Authentication Modules (PAM) and its significance in Linux-based systems. From understanding its core principles to constructing our custom module using Golang, we've achieved a comprehensive grasp of this integral system component.
Key takeaways include:
- Introduction to PAM: We explored how PAM modularizes the authentication mechanisms of various services on Linux, allowing for flexibility and integration with various authentication providers.
- Building a Custom Module: Utilizing Golang, we've successfully crafted our module, interfacing with the PAM API and integrating it with an external authentication provider (Keycloak).
- Deployment: Beyond just creation, we've covered the steps to deploy our module in the proper directories and have integrated it with various services by modifying their respective PAM configuration files.
- Testing: We've underscored the importance of thorough testing and walked through the steps for the same, ensuring our module behaves as intended across different services.