Pluggable Authentication for the Masses
Recently I was talking with someone who expressed an interest in allowing their users to specify that their account can only be logged into using an SSH key only and thus not with a password. They wanted to allow this to be done by the user -- similar to how SSH's "authorized_keys" is done.
I suggested that they write a simple PAM module to accomplish this, but they indicated that they felt intimidated by that prospect. So here I am, to walk you all through this relatively simple process.
Typically I develop an application iteratively in such a way that I always have an application that compiles and doesn't do anything adverse. So I will walk through the steps I take to build any PAM module first, then add the specific functionality functional part by functional part.
First, we'll start with a basic PAM module that returns "ignore" for everything:
/* Define which PAM interfaces we provide */
#define PAM_SM_ACCOUNT
#define PAM_SM_AUTH
#define PAM_SM_PASSWORD
#define PAM_SM_SESSION
/* Include PAM headers */
#include <security/pam_appl.h>
#include <security/pam_modules.h>
/* PAM entry point for session creation */
int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return(PAM_IGNORE);
}
/* PAM entry point for session cleanup */
int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return(PAM_IGNORE);
}
/* PAM entry point for accounting */
int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return(PAM_IGNORE);
}
/* PAM entry point for authentication verification */
int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return(PAM_IGNORE);
}
/*
PAM entry point for setting user credentials (that is, to actually
establish the authenticated user's credentials to the service provider)
*/
int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return(PAM_IGNORE);
}
/* PAM entry point for authentication token (password) changes */
int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return(PAM_IGNORE);
}
So now we have our PAM module that just does nothing. Let's go ahead and compile it (Linux compilation specified):
user@build$ gcc -fPIC -DPIC -shared -rdynamic -o pam_ignore.so pam_ignore.c
Or, if you are running a multilib system, you will need to compile the PAM module for every architecture your system has a "libpam" for, for example for Linux/x86_64 and Linux/i386:
user@build$ gcc -m32 -fPIC -DPIC -shared -rdynamic -o pam_ignore_32.so pam_ignore.c
user@build$ gcc -m64 -fPIC -DPIC -shared -rdynamic -o pam_ignore_64.so pam_ignore.c
Next, we'll install the PAM module into the path where it should live. On most Linux systems this is "/lib/security" (or "/lib/security" for 32-bit and "/lib64/security" for 64-bit libraries on 32/64 multilib systems)
root@test# cp pam_ignore_32.so /lib/security/pam_ignore.so
root@test# cp pam_ignore_64.so /lib64/security/pam_ignore.so
root@test# chown root:root /lib/security/pam_ignore.so /lib64/security/pam_ignore.so
root@test# chmod 755 /lib/security/pam_ignore.so /lib64/security/pam_ignore.so
Finally, we should configure our PAM implementation to actually use the module. This is where first have to start making decisions on how our system should interact with our PAM module. To determine this, we go back to our problem and back to talking about how PAM actually works.
We want SSH to allow all users to login using their SSH keys but selectively deny access using a password. Typically this kind of decision would be made by an "account" interface within a module. The "account" interface is for determining if an account is valid for this login, so it would return PAM_PERM_DENIED if the user designated this so. But by the time we are processing the "account" interface the user has already either authenticated via SSH keys or via password -- and we don't know which one has taken place !
There are several ways to resolve this dilemma, along two basic lines of reasoning. The first line of reasoning is that we can use the "authentication" interface of PAM module to store which one happened and then later retrieve that information from the "account" interface of the PAM module. The second line of reasoning is that the "authentication" interface of PAM modules are indeed only called from SSH during non-SSH-key based authentication so we could just return failure there.
There are benefits to each approach, but for simplicity we will use the latter approach.
So, given that, we are now ready to insert our PAM module into the PAM configuration. We have determined that we only care about it's "authentication" interface, and only for SSH. How we insert this depends on your PAM configuration, but on Linux it's typically done by editing "/etc/pam.d/sshd". For our module we will insert above all the other "auth" modules, so something like (only the top-line is added, the second line is an example of what might already exist):
auth requisite pam_ignore.so
auth include system-auth
There are some important details about PAM at this point:
- PAM directives are followed in-order
- There is the concept of a "result" of a PAM module (Success, Failure, Ignore, Error)
- The "action" (requisite, required, sufficient, etc) indicates what exactly is done with the result
A very important note here about the "action" we used above, "requisite", it means that if our PAM module returns in failure the failure is immediately returned to the application to return to the user. We do this because we don't want something like "pam_tally" counting this as an authentication attempt and failure. If success is returned, it could be considered successful authentication if nothing else in the "PAM stack" returns any failures. We don't want to do that since we don't actually check anyone's authentication token (password) so we will only ever return PAM_IGNORE or PAM_AUTH_ERR.
Alright, so now we have our PAM module in place and being used. We should ensure that we can still login to our test system at this point. We should do this with a new connection and leave our existing root session alone in case we need to undo the changes.
After we have verified that our PAM module is operable and we can still authenticate to the system we can move on to making our PAM module actually do something. Finally.
Since we are only providing an "authentication" interface we will only be modifying the pam_sm_authenticate function and I will not repeat the other functions or the C headers we included. However, your source code should be the aggregate of this modification and the original above.
The first thing we want our authentication system to do is identify the user that is attempting to authenticate. Conveniently PAM provides us with a function that does just that -- pam_get_user(). Since we will be linked with PAM at run-time by any application that uses us we do not need to indicate at compile-time that we depend on it.
pam_get_user() takes three (3) arguments:
- The PAM handle (pamh);
- A place to store the username (user); and
- An optional prompt, if the username already has not been collected (prompt)
These are all relatively easy to satisfy. The function we are operating inside of already provides us with the PAM handle (pamh) to provide. We should already have the username, so we don't care too much about the prompt. The most difficult thing to do is provide a place to store the username. The documentation indicates that this parameter is a pointer to a C-style string (pointer to a char), and that we do not need to free() it later. This makes it easy on us.
Alright, so let's start with that:
#include <unistd.h>
int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
const char *user = NULL;
int pgu_ret;
pgu_ret = pam_get_user(pamh, &user, NULL);
if (pgu_ret != PAM_SUCCESS || user == NULL) {
return(PAM_IGNORE);
}
return(PAM_IGNORE);
}
Now we have the username. The next step is to actually check for the file that we want to use to determine if we should succeed or fail. Let's define this as "<user_home_dir>/.ssh/nopasswd". So that means we need to find the user's home directory.
In order to get the user's home directory from what we know of the user (their username) we will need to use one of the functions in the getpwnam-family of fuctions. Specifically we will use getpwnam_r() since PAM modules need to be re-entrant since they are linked into applications that may have multiple threads doing unrelated things.
So our code with getpwnam_r() added in looks like this:
#include <unistd.h>
#include <pwd.h>
int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
struct passwd *pw = NULL, pw_s;
const char *user = NULL;
char buffer[1024];
int pgu_ret, gpn_ret;
pgu_ret = pam_get_user(pamh, &user, NULL);
if (pgu_ret != PAM_SUCCESS || user == NULL) {
return(PAM_IGNORE);
}
gpn_ret = getpwnam_r(user, &pw_s, buffer, sizeof(buffer), &pw);
if (gpn_ret != 0 || pw == NULL || pw->pw_dir == NULL || pw->pw_dir[0] != '/') {
return(PAM_IGNORE);
}
return(PAM_IGNORE);
}
Next, we'll construct the path to the file to check using snprintf() and actually check for it, using access():
#include <unistd.h>
#include <stdio.h>
#include <pwd.h>
int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
struct passwd *pw = NULL, pw_s;
const char *user = NULL;
char buffer[1024], checkfile[1024];
int pgu_ret, gpn_ret, snp_ret, a_ret;
pgu_ret = pam_get_user(pamh, &user, NULL);
if (pgu_ret != PAM_SUCCESS || user == NULL) {
return(PAM_IGNORE);
}
gpn_ret = getpwnam_r(user, &pw_s, buffer, sizeof(buffer), &pw);
if (gpn_ret != 0 || pw == NULL || pw->pw_dir == NULL || pw->pw_dir[0] != '/') {
return(PAM_IGNORE);
}
snp_ret = snprintf(checkfile, sizeof(checkfile), "%s/.ssh/nopasswd", pw->pw_dir);
if (snp_ret >= sizeof(checkfile)) {
return(PAM_IGNORE);
}
a_ret = access(checkfile, F_OK);
if (a_ret == 0) {
/* The user's file exists, return authentication failure */
return(PAM_AUTH_ERR);
}
return(PAM_IGNORE);
}
We should probably name this something other than "pam_ignore" at this point, perhaps something like "pam_ssh_denypasswd" or similar.
Then we compile, install, and test as above and declare victory.
Hooray.
Postscript:
If you wish to take the PAM module further it's a good idea to start to incorporate GNU autoconf at this point to support multiple platforms. I have a skeleton similar to "pam_ignore" called "pam_success" that can be used for this purpose.
Top comments (0)