Using 2 factor authentication with OpenSSH
Since OpenSSH can delegate the authentication to PAM, it's easy to extend a password-based authentication to a full 2 factor authentication. Since OpenSSH 6.2, it is also possible to check for a second factor after another method, for example using SSH keys, using AuthenticationMethods
.
However, we want more, we want our users to be able to use any of their authentication methods (password, SSH keys, Kerberos, ...) and propose them a second factor authentication afterwards. But let's start step by step from the basis and build our solution
The basis: 2 factor authentication with pam
PAM is by design modulable. It's possible to build your own module, compile it and by simply adding it to the right folder, PAM will be able to use it (after some configuration). You can find some existing module online or build your own based on them, for example:
- https://github.com/Yubico/yubico-pam
- https://github.com/duosecurity/duo_unix
- https://github.com/CERN-CERT/pam_2fa
The PAM configurations is always found in the /etc/pam
folder, and for sshd, this is /etc/pam.d/sshd
. This configuration file obeys a very simple format, defined in its documentation. For most systems, /etc/pam.d/sshd
will only contain an include
control definition for the auth
service like this one:
auth include password-auth
We need two changes in this configuration:
- Replacing the
include
by ansubstack
. This will only change the way success and errors are handled. Instead of immediately stop and succeed/fail, PAM will continue through. This allows us to include anotherrequired
rule without being worried by asufficient
rule to stop the authentication beforehand. - Add a new
required
rule which will load our new second factor module, with, if required, its configuration.
For CERN, this results in the following configuration:
auth substack password-auth
auth required pam_2fa.so [CERN-specific configuration]
And Voila, we have successfully configured PAM to use 2 factor authentication for sshd. Now we only need to configure sshd to use PAM via the following configuration:
UsePAM yes # That's easy!
ChallengeResponseAuthentication yes # To allow the client to discuss with the remote PAM
RSAAuthentication no # We can't support any non-pam authentication
PubkeyAuthentication no # We can't support any non-pam authentication
PasswordAuthentication no # No basic password authentication
KerberosAuthentication no # We can't support any non-pam authentication
Just restarting our sshd service and we're done, we've protected our servers with 2 factor authentication! But this was only the beginning: we need to support Kerberos, not passwords.
AuthenticationMethods: supporting Kerberos
In OpenSSH 6.2, a new parameter was included: AuthenticationMethods
. This new feature allows to define chains of authentication methods that are required. The design is relatively simple: the server exposes a list of method the client can use, the client pick one and use it, the servers then replies "good, now pick another one in this list", rinse&repeat until satisfied.
So we want to use Kerberos. Looking at the documentation, the corresponding method is gssapi-with-mic
. Now, our second factor is using PAM, so we still need the corresponding method, keyboard-interactive:pam
. We just have to chain the two, re-enable the corresponding authentication and here is our configuration:
UsePAM yes # That's easy!
ChallengeResponseAuthentication yes # To allow the client to discuss with the remote PAM
KerberosAuthentication yes # We support it!
AuthenticationMethods gssapi-with-mic,keyboard-interactive:pam
RSAAuthentication no # We don't support it
PubkeyAuthentication no # We don't support it
PasswordAuthentication no # No basic password authentication
We still have a small problem as we didn't change our PAM configuration: our users will be asked to type their password. We just need to remove the password authentication and simply keep our second factor module in PAM:
auth required pam_2fa.so [CERN-specific configuration]
Restarting sshd, and done!
AuthenticationMethods: adding SSH keys support
On top of Kerberos, we also want to support SSH keys. Looking at OpenSSH naming convention, the method for it is publickey
. We just need to enable it and chain it to keyboard-interactive:pam
and add it to the list of supported methods in our configuration:
UsePAM yes # That's easy!
ChallengeResponseAuthentication yes # To allow the client to discuss with the remote PAM
KerberosAuthentication yes # We support it!
PubkeyAuthentication yes # We support it!
AuthenticationMethods gssapi-with-mic,keyboard-interactive:pam publickey,keyboard-interactive:pam
RSAAuthentication no # We don't support it
PasswordAuthentication no # No basic password authentication
AuthenticationMethods: adding PAM support ?
And now we are back to the beginning, supporting password-based authentication, but this time in addition to the other methods. Password-based authentication is using keyboard-interactive:pam
, so if we were to follow the previous logic, we would use keyboard-interactive:pam,keyboard-interactive:pam
. However, if we do so, we still only have one single PAM configuration in which we can:
- Ask for the second factor only: the normal password will never be asked, at most we can get the second factor twice...
- Ast for the password only: the second factor will never be asked, the password will be asked instead, even after kerberos and ssh key authentication
- Ask for both: this make password-authentication work (given that we only put
keyboard-interactive
once), but break Kerberos and ssh key authentication as the password will also be asked...
There is no such thing as keyboard-interactive:pam:sshdpassword
or keyboard-interactive:pam:2fa
. There is only one configuration which is always called in the same environment. In the end, PAM can't differentiate the two cases we have: calling PAM after a successful authentication for the second factor or calling PAM for doing the whole authentication...
Exposing more information to PAM
In the end we have no choice but to patch OpenSSH to expose more information to PAM, allowing it to make the right decision. The patch was proposed to the OpenSSH bug tracker and RedHat agreed to include a version of this patch in CentOS 7.3+
This patch adds a new environment variable, SSH_USER_AUTH, which is exposed:
- During the authentication via the PAM environment. It contains the list of successful authentications methods so far.
- After the authentication, when a new shell is spawn, via the shell environment. It contains the list of authentications methods successfully used to login into the server.
This list of successful authentication methods also contain:
- The public key fingerprint for key-based authentications
- The host public key fingerprint for host-based authentications
- The Kerberos principal for Kerberos authentication
Our patch has been adapted by upstream and a modified version is already included since 7.6. Unfortunately this patch is missing a key functionality and thus currently does not work for us.
Using a smart PAM configuration
With this patched version of OpenSSH, it is now possible to make decisions based on this environment variable. Using this variable is safe as it is only controlled by:
- sshd, that refreshes its value before any pam authentication call
- pam modules themselves during a single call, as they can modify the environment
Let's start with a simple check: the existence of a non-empty SSH_USER_AUTH variable in the PAM environment. A straightforward version of such a module is available on our GitHub. This module will basically return two values: PAM_SUCCESS if there was any previously successful authentications and PAM_IGNORE if there was none. Based on this, we can write the following PAM configuration:
auth [success=1 ignore=ignore default=die] pam_ssh_user_auth.so
auth substack password-auth
auth required pam_2fa.so [CERN-specific configuration]
This configuration is simple, but not trivial to read, as the format is not simple, especially on the first line. This first line means that pam will call pam_ssh_user_auth.so and will:
- on PAM_SUCCESS (i.e. if there was any previous successful sshd authentication) skip the next line which contains the password authentication
- on PAM_IGNORE (i.e. if there was no previous successful sshd authentication) ignore the result and simply continue
- on any other value (bug?) just die
We can ensure the behavior we want, that is running the 2nd factor after any first factor, by configuring SSHD in the following manner:
UsePAM yes # That's easy!
ChallengeResponseAuthentication yes # To allow the client to discuss with the remote PAM
KerberosAuthentication yes # We support it!
PubkeyAuthentication yes # We support it!
AuthenticationMethods gssapi-with-mic,keyboard-interactive:pam publickey,keyboard-interactive:pam keyboard-interactive:pam
RSAAuthentication no # We don't support it
PasswordAuthentication no # No basic password authentication
Protecting the second factor
At CERN, we have another requirement: the second factor should never be asked is there was no first factor. This is due to the fact that one of the second factor we are using is based on SMS and we don't want malicious attackers to spam our users with SMS...
Due to this constrain, we need a more complicated PAM configuration, which I won't explain here (consider it an exercise for the reader):
auth [success=2 ignore=ignore default=die] pam_ssh_user_auth.so
auth substack password-auth
auth [default=1] pam_deny.so
auth required pam_2fa.so [CERN-specific configuration]
This new configuration will result in a single pam call resulting in:
- Password authentication (only) if there was no previous successful sshd authentication
- Second factor authentication (only) if there was any previous successful sshd authentication
As a result, we need to require two successful
keyboard-interactive:pam
by themselves and not only one:
UsePAM yes # That's easy!
ChallengeResponseAuthentication yes # To allow the client to discuss with the remote PAM
KerberosAuthentication yes # We support it!
PubkeyAuthentication yes # We support it!
AuthenticationMethods gssapi-with-mic,keyboard-interactive:pam publickey,keyboard-interactive:pam keyboard-interactive:pam,keyboard-interactive:pam
RSAAuthentication no # We don't support it
PasswordAuthentication no # No basic password authentication
Acknowledgements
We would like to thanks:
- György Demarcsek for his initial idea and patch
- Damien Miller for his name and format suggestions
- Radoslaw Ejsmont for following-up upstream modification and identifying the incompatibilities