2-factor authentication & writing PAM modules for Ubuntu



The problem

Passwords are often seen as a weak link in the security of today’s I.T. infrastructures. And justifiably so:

  • re-usability, which we’re all guilty of, guarantees that credentials compromised on a system can be leveraged on many others. And given the world we live in, password re-use is inevitable, we just have too many accounts in too many places.
  • plain text protocols are still used to transmit credentials, and the result is that they are exposed to network sniffing. This is worsened by the increase in wireless usage which broadcasts information. Telnet, FTP, HTTP come to mind but they aren’t the only ones.
  • lack of encryption on storage is a flaw that too often makes it way into architecture design. How many databases have we heard about getting hacked & dumped? How many have we not heard about?
  • password simplicity & patterns are also factors weakening us against bruteforce attacks.

So far, the main counter measure we’ve see out there is complexity enforcement. Sometimes IP restriction, or triggering warnings on geographic inconsistencies (Gmail, Facebook). But these barely help alleviate problem.

A solution

One hot solution that is making its way into critical systems (banks, sensitive servers) is Multi-factor authentication, and by “multi” we’ll stick to 2-factor authentication (2FA) because, well 3 factor authentication might be getting a little cumbersome :). The goal is to have more than one mean of establishing identity. And as much as possible, the means have to be distinct in order to reduce the chances of having both mechanisms compromised.

Let’s see how to implement 2FA on an Ubuntu server for SSH. Ubuntu uses PAM (Pluggable Authentication Modules) for SSH authentication among other things. PAM’s name speaks for itself, it’s comprised of many modules that can be added or removed as necessary. And it is pretty easy to write your own module and add it to SSH authentication. After PAM is done with the regular password authentication it already does for SSH, we’ll get it to send an email/SMS with a randomly generated code valid only for this authentication. The user will need access to email/cell phone on top of valid credentials to get in.


Let’s do an ls on /lib/security, this is where the pam modules reside in Ubuntu.

Let’s go ahead and create our custom module. First, be very careful, we’re messing with authentication and you risk locking yourself out. A good idea is to keep a couple of sessions open just in case. Go ahead and download the source for our new module.

Take a look at the code, you’ll see that PAM expect things to be laid out in a certain way. That’s fine, all we care about is where to write our custom code. In our case it starts at line 35. As you can see, the module takes 2 parameters, a URL and the size of the code to generate. The URL will be called and passed a code & username. It is this web service that will be in charge of dispatching the code to the user. This step could be done in the module itself but here we have in mind a centrally managed service in charge of dispatching codes to multiple users.

Deploying the code is done as follows:

gcc -fPIC -lcurl -c 2ndfactor.c
ld -lcurl -x --shared -o /lib/security/2ndfactor.so 2ndfactor.o

If you got errors, you probably need to first:

apt-get update
apt-get install build-essential libpam0g-dev libcurl4-openssl-dev

Do an ls on /lib/security again and you should see our new module, yay!

Now let’s edit /etc/pam.d/sshd, this is the file that describes which PAM modules take care of ssh authentication, account & session handling. But we only care about authentication here. The top of the file looks like:

# PAM configuration for the Secure Shell service

# Read environment variables from /etc/environment and
# /etc/security/pam_env.conf.
auth       required     pam_env.so # [1]
# In Debian 4.0 (etch), locale-related environment variables were moved to
# /etc/default/locale, so read that as well.
auth       required     pam_env.so envfile=/etc/default/locale

# Standard Un*x authentication.
@include common-auth

The common-auth is probably what takes care of the regular password prompt so we’ll add our module call after this line as such:

auth       required     2ndfactor.so base_url=http://my.server.com/send_code.php code_size=5

The line is pretty self descriptive: this is an authentication module that is required (not optional), here’s its name and the parameters to give it.

send_code.php can be as simple as:

<?php mail( "{$_GET['username']}@mail_server.com", "{$_GET['code']}" ) ; ?>

Or a complex as you can make it for a managed, multi-user, multi-server environment.

Lastly, edit /etc/ssd/sshd_config and change ChallengeResponseAuthentication to yes. Do a quick

/etc/init.d/ssh restart

for the change to take effect.

That’s it! try and ssh in, the code will be dispatched and you will be prompted for it after the usual password. This was tested on Ubuntu 10.04 32b / Ubuntu 10.04.2 64b / Ubuntu 11.04 64b / Ubuntu 12.04 64b.

A few disadvantages of this 2FA implementation worth mentioning
  • more steps required to get in
  • doesn’t support non TTY based applications
  • relying on external services (web service, message delivery), thus adding points of failure. Implementing a fail-safe is to be considered.
  • SSH handles key authentication on its own, meaning a successful key auth does not go through PAM and thus does not get a chance to do the 2nd factor. You might want to disable key authentication in sshd’s config.

34 Replies to “2-factor authentication & writing PAM modules for Ubuntu”

  1. Greetings,

    I have just recently completed a pam module for two factor authentication against Microsoft’s Azure Active Directory.

    It’s currently in Beta, but it works fine. The module implements the device code oauth2 workflow for the Microsoft Graph API via libcurl.

    URL: https://github.com/CyberNinjas/pam_aad

    I’m personally not a fan of Microsoft, but we use their services at work. At some point in the future, I’d like to use the knowledge and experienced gained by working on this module, to write a generic pam_oauth2 module, to authenticate against any oauth2 provider.

    Note: this comment reflects my personal opinions, and is not endorsed by my current, or any future employers.

    1. Great, happy to link.

      And yes, incredible how far Microsoft goes on inertia alone with such horrible software. I too moved my stuff off Github promptly after they bought it knowing how they ruin anything they touch. Worst of all they bought Minecraft and now my son and I can’t play in the worlds we built, bunch of fucking pricks I tell you.

  2. I have a task to add 2FA to su/sudo
    can this be applied to those authentication/elevation mechanisms?

    Also, we use AIX 7.x, is there any guidance you could provide to modify this to work with AIX?

    1. At least in the Linux world, sudo triggers a PAM chain. It can be found there: /etc/pam.d/sudo. I bet it’s pretty simple to add this PAM module to it.

      Although I’m a fish out of water in AIX. I’m not sure it even uses PAM, if it does it’s probably a reasonable process to compile and insert this module. I recommend compiling a module which does nothing else than touch a file in /tmp, just to make sure you can compile and insert it before it does anything fancier.

      You also might want to take a look at https://duo.com/docs/duounix

      They’re pretty solid and provide PAM modules for AIX it seems.

  3. Hello Ben.
    Thanks for this amazing tutorial.
    I’m trying to setup PAM where the 2nd factor is generated by my own algorithm. Do you have any similar material? Or how can I have this code retrieve the second factor from a code generator running on the same server?

    1. Thanks for the kind words; what you describe is almost what’s going on here.

      In /etc/pam.d/sshd we call our second factor with a couple of parameters. One is a URL which we use to dispatch the code we generated in the module. I think it would be easy to have that URL take care of generating the code as well, and pass it back in that same curl call to the PAM module for later comparison.

      This way you can have a centralized script do the generation and the dispatching. Here the generation is in the module and we rely on that URL only for dispatching.

      I chose a URL because I liked the idea that multiple servers could hit the same logic. This allows one to maintain a list of users and restrictions centrally. But you could summon local scripts on the local server as well.

      I hope this helps.

  4. Can’t get the output of any of the printf or fprintf statements in the console window. Any idea why ? Gives the access and compiles without any warnings or errors.
    OS: Ubuntu 14.04

  5. Thanks a lot ! Great way to get started with PAM 🙂 works perfectly. Best article I found as I want to write my own PAM module for SSH !

  6. Hi Ben,

    I want to test it on Scientific Linux 6.4. I have done all the steps what you have mentioned. but when I am using ssh then it is only asking password and no second factor authentication it is asking.

  7. Hi,

    There is a security problem with the PAM module in your post. I couldn’t find your email address on this blog so I’m just putting it here into the comments.

    Specifically, the problem is the following code to generate the tokens:
    /* generating a random one-time code */
    char code[code_size+1] ;
    unsigned int seed = (unsigned int)time( NULL ) ;
    srand( seed ) ;
    for( i=0 ; i<code_size ; i++ ) {
    int random_number = rand() % 10 ; // random int between 0 and 9
    code[i] = random_number + 48 ; // +48 transforms the int into its equivalent ascii code
    code[code_size] = 0 ; // because it needs to be null terminated

    rand() will always return the same sequence for a given seed. In this case you call srand() to seed the RNG, but the only input is the current timestamp. So it is possible for an attacker to create valid tokens by calling srand(current_time_t) and applying the same transform as you do in the algorithm above. Due to clock skew, he won't have exactly the same time down to the second, but it would be easy to guess (and can probably be checked using other services on the server that print the current time, such as HTTP error pages etc.)

    A better way would be to get the random values from /dev/random on linux. I will be modifying the code for this purpose, along with making it call a shell script rather than an HTTP service.

    1. This is a very good point, I actually thought about it initially but I considered the code random enough.

      The main idea behind 2 factor auth isn’t to find the most mathematically random number, it is to use a second medium for authentication. Now of course we’re not going to hardcode “1234” but even then I’d get an SMS and know that something is going on.

      When you think about it, sending 4 digits code offers only 10000 permutations which is statistically about a day’s worth of work at 1 attempt per second. Not very secure but hopefully I’ve gotten off my ass after 100 SMSes 🙂

      I concede that it is a shortcoming and that while making security software, I might as well make it 100% right. I’ll fix it, thank you for bringing this up.

      Also, if all you are trying to do is fire a shell script, you might be interested in http://ben.akrin.com/?p=1290 , you will be pleased to know that it uses /dev/urandom 😀

      take care.

      1. Ok fixed 🙂

        Random code generation looks like that now:
        /* generating a random one-time code */
        char code[code_size+1] ;
        unsigned int random_number ;
        FILE *urandom = fopen( “/dev/urandom”, “r” ) ;
        fread( &random_number, sizeof(random_number), 1, urandom ) ;
        fclose( urandom ) ;
        snprintf( code, code_size+1,”%u”, random_number ) ;
        code[code_size] = 0 ; // because it needs to be null terminated

  8. Hi,

    first of all thanks for the answer.

    It’s not my original idea, I asked to do that, so…I have to do or make something similar. It looks a little bit complex to me do that modules modifications.

    The idea is that if someone makes you (facet to face, with violence) to access to your pc, you can type the second password and a non-sensibility data home appears, without important files and some looks-like important garbage, so you real encrypted home is safe and the attackers let you go. In the normal life, you use the first password to access to your real home.

    My another idea to do that is to make 2 different users, with 2 different ecnrypted homes, in the normal way, and not show on the GMD login the users name, so nobody knows how many users there are, so you can type the second username and access the second home folder, but I don’t know if makes the same effect or what.

    Thanks for your help and time.

  9. Hi,

    thanks for the module.

    I want to change the Ubuntu login to have only one user with 2 or multi passwords. Depending on what password you enter, you will have access to a different folder (/home, /home1, etc).

    Do you know if it’s possible with PAM to do this? I don’t know and will be great to know it, to continue searching or discard it.

    Thank you in advance. Regards.

    1. Do you really, absolutely have to do this based on the password? Different credentials lead to different home dirs that’s nothing new but as you know it’s usually based on the username not the password. Anyway, I’m sure this can be done (there’s always a way) but you can’t really do that by adding a PAM module to your chain; it seems as though you will want to code your own version of the module that does authentication rather.

      A look at /etc/pam.d/sshd points to:
      > 10 # Standard Un*x authentication.
      > 11 @include common-auth

      So then we look at /etc/pam.d/common-auth:
      > 17 auth [success=2 default=ignore] pam_unix.so nullok_secure

      and it looks like pam_unix.so is the module you want to make your own version of.

      But you know, the more I think about it, the more I think it’s tricky. It’s tricky because your session gets instantiated post-authentication by the SSH server, so somehow you’d want whichever modifications you make in the authentication module to be able to carry through the rest of the PAM chain back to SSH which actually instantiates your shell; not impossible but it will definitely add complexity.

      That’s as far as I have time to go on this idea, it looks like you are going to have to get your hands dirty to get something of the sort working. Have you thought about maybe just adding a prompt to ask users which home dir they want?

      I hope this helps a little.

    1. Thank you for sharing your findings. I haven’t had time to get that ArchLinux VM working but I plan on it in the near future. If you changed some code in 2ndfactor.c I’d be interested in knowing what it looks like.

      I’m glad you got it working!

  10. Hello! I’m trying to get this to work on my ArchLinux box, but to no avail. I have the libraries installed, and the header files, but I am unable to get it to work with SSH. I get:

    15:29:56 localhost sshd[1963]: PAM unable to resolve symbol: pam_sm_authenticate
    15:29:56 localhost sshd[1963]: PAM unable to resolve symbol: pam_sm_setcred
    15:30:05 localhost sshd[1963]: error: PAM: Module is unknown for USER from IP
    15:30:12 localhost sshd[1963]: error: PAM: Module is unknown for USER from IP
    15:30:16 localhost sshd[1963]: error: PAM: Module is unknown for USER from IP

    The prompt never even shows up. Any idea on what I need to do? If you’re not too busy, I’d appreciate some advice. Thank you!

      1. Well so I’m having the weirdest bug with an Arch VM running in VirtualBox so I haven’t even been able to give it a shot yet. It’ll be a while longer.

        1. Hey Ben, I managed to get something working but I’m not quite sure how or why. It may have been a different compiling/linking process (I ended up changing a lot of other things though; basing it off of a different tutorial, the URL of which I’ve since forgotten.)

          Anyway, I compiled it like so:

          gcc -fPIC -DPIC -shared -rdynamic -o 2ndfactor.so /usr/lib/libpython2.7.so 2ndfactor.c

          Thanks so much for taking time to try to help with this, though.

  11. What i want is…I have written my script for taking image of Login person. I run this script at the start of System it works.But what if i run this at event when user login fail, lets say after 3 wrong attempts.
    Is there any need to make PAM module to do this?
    I don’t have any experience with pam programing.

    As you are looking smart in PAM so i can hope you have my answer/solution.
    This is my college project.And some more modules are there but its enough for me to learn this thing first.
    Thank you. I am waiting for reply.[mail:-sxxxxxxxxxxxxxxx1@gmail.com]

    1. Sachin,

      I’m not going to do your college project for you :), but I’m happy to give pointers.

      The easiest way to keep track of how many failed attempts have happened is to log them into a file in /tmp for example.

      You can have your PAM module check that file before anything at the beginning of the pam_sm_authenticate function.
      First, add the current attempt to the file.
      Then check that same file to see how many attempts occurred recently
      – If there weren’t many attempts, just “return PAM_SUCCESS”
      – Otherwise, if there were, you can proceed with the extra check (2nd factor)
      Lastly don’t forget to cleanup the file every now and then so it doesn’t grow to an unmanageable size.

      That’s if you decide to use a PAM module but you don’t have to; you might be interested in http://ben.akrin.com/?p=1290 if you want to avoid the complexity of a PAM module. With ForceCommand you can use the scripting language of your choice and the operating principle would be the same as described above.

  12. I am trying to run it in Fedora but it is not working. It doesnt give any error it just dont appear what the pam module that I wrote. I made a pam_module that display a message. Can you guide me how to do it in Fedora?

Leave a Reply

Your email address will not be published. Required fields are marked *