Two factor ssh login, Google authenticator and SELinux

November 15, 2012 - 11 min read
Old post, probably a better way

I'm not sure how relevant this post is anymore. There is probably a better way of doing this now, and the real problem is probably fixed as well.

Keeping it around for historical reasons.

There is too many people that literally hate SELinux, and comes to the conclusion that it is way to complicated or unfriendly and just ends up turning it off instead of trying to fix it so you can live with it.

I want two factor authentication on one of my ssh servers. Google authenticator is todays perfect solution for this. It is well made, support many different platforms on the client side, have a pam module, is opensourced and build on open standards. Most major Linux distroes have a package for it, so it should be easy enough to install. On Scientific Linux (a great distro btw) or any other RedHat based distro it is already in the EPEL repository called google-authenticator.

This article isn't about setting up Google authenticator, there is plenty of blog articles about that already, on How-to-geek, mnxsolutions or other places Google will take you.

When you are done with the initial Google authenticator setup, you should have a file in your home directory called .google_authenticator, this file contains your authenticator secret, and other information the authenticator needs to log you in and keep track of what token is valid.

But to get it to work with SELinux can be a little more tricky. Here is my process from start to finish.

Making a plan

If you try to login now, you won't probably even see the Google authenticator ask for the token. This is because SELinux blocks sshd from reading random files in the users home directory. You can see this normally in /var/log/secure with an entry like this:

Nov 12 23:34:49 omi sshd(pam_google_authenticator)[3350]: Failed to read "/home/xeor/.google_authenticator"

One way to find out what went wrong is to use SELinux's audit2allow tool like this.

grep ssh /var/log/audit/audit.log | audit2allow
#============= sshd_t ==============
#!!!! The source type 'sshd_t' can write to a 'file' of the following types:
# user_tmp_t, auth_cache_t, faillog_t, ssh_home_t, pam_var_run_t, pcscd_var_run_t, sshd_var_run_t, gitosis_var_lib_t, sshd_tmpfs_t, var_auth_t, root_t, krb5_host_rcache_t

allow sshd_t user_home_dir_t:file { rename write getattr read create unlink open };

As you see, to fix the "failed to read" error, this is the SELinux module you have to make. There is several thing to take a note of based on this output and our current findings:

  • This is the errors generated by SELinux when SELinux is already in deny mode! This means that this is probably the first of many errors we will meet.
  • The allow rule audit2allow recommends is way to wide. It basically suggest that the server running as the sshd-type should have read/write/delete/+ info in the whole users home directory. This will defeat the purpose of having all this rules on sshd to lock it down.
  • sshd already have write access to a bunch of files. We will use semanage to find out exactly which directories this is.

To get a list of paths that SELinux changes context in, use semanage fcontext -l. The list you will get is the list for your current user. Like in the list below, you will see that the SELinux type sshhome_t belongs to the files in /root/. That is because that is my current home directory.. More on this _magic later in the article.

semanage fcontext -l | grep -E "user_tmp_t|auth_cache_t|faillog_t|ssh_home_t|pam_var_run_t|pcscd_var_run_t|sshd_var_run_t|gitosis_var_lib_t|sshd_tmpfs_t|var_auth_t|root_t|krb5_host_rcache_t"
/                                                  directory          system_u:object_r:root_t:s0 
/initrd                                            directory          system_u:object_r:root_t:s0 
/root/\.shosts                                     all files          system_u:object_r:ssh_home_t:s0 
/root/\.ssh(/.*)?                                  all files          system_u:object_r:ssh_home_t:s0 
/var/cache/coolkey(/.*)?                           all files          system_u:object_r:auth_cache_t:s0 
/var/cache/krb5rcache(/.*)?                        all files          system_u:object_r:krb5_host_rcache_t:s0 
/var/lib/abl(/.*)?                                 all files          system_u:object_r:var_auth_t:s0 
/var/lib/amanda/\.ssh(/.*)?                        all files          system_u:object_r:ssh_home_t:s0 
/var/lib/gitolite(/.*)?                            all files          system_u:object_r:gitosis_var_lib_t:s0 
/var/lib/gitolite/\.ssh(/.*)?                      all files          system_u:object_r:ssh_home_t:s0 
/var/lib/gitosis(/.*)?                             all files          system_u:object_r:gitosis_var_lib_t:s0 
/var/lib/pam_shield(/.*)?                          all files          system_u:object_r:var_auth_t:s0 
/var/lib/pam_ssh(/.*)?                             all files          system_u:object_r:var_auth_t:s0 
/var/log/btmp.*                                    regular file       system_u:object_r:faillog_t:s0 
/var/log/faillog                                   regular file       system_u:object_r:faillog_t:s0 
/var/log/tallylog                                  regular file       system_u:object_r:faillog_t:s0 
/var/run/faillock(/.*)?                            all files          system_u:object_r:faillog_t:s0 
/var/run/pam_mount(/.*)?                           all files          system_u:object_r:pam_var_run_t:s0 
/var/run/pam_ssh(/.*)?                             all files          system_u:object_r:var_auth_t:s0 
/var/run/pcscd\.comm                               socket             system_u:object_r:pcscd_var_run_t:s0 
/var/run/pcscd\.events(/.*)?                       all files          system_u:object_r:pcscd_var_run_t:s0 
/var/run/pcscd\.pid                                regular file       system_u:object_r:pcscd_var_run_t:s0 
/var/run/pcscd\.pub                                regular file       system_u:object_r:pcscd_var_run_t:s0 
/var/run/sepermit(/.*)?                            all files          system_u:object_r:pam_var_run_t:s0 
/var/run/sshd\.init\.pid                           regular file       system_u:object_r:sshd_var_run_t:s0 
/var/run/sudo(/.*)?                                all files          system_u:object_r:pam_var_run_t:s0 
/var/tmp/HTTP_23                                   regular file       system_u:object_r:krb5_host_rcache_t:s0 
/var/tmp/host_0                                    regular file       system_u:object_r:krb5_host_rcache_t:s0 

Ok, so ~/.ssh/ kinda looks good, but that won't work without some pam configuration. Lets try to create an ssh SELinux module instead and use the default /home/username/.google_authenticator location.

Making a SELinux module

audit2allow supports an -M modulename option that creates the module for you based on what you pipe to it. However, we will make this one from scratch, since its easier to learn, and easier to test and maintain.

First, create a folder for our module, lets just put it in /root/selinux/modules/sshd_google_authenticator/ for now. Go into it. Make sure you can find the selinux-devel make file, usually located at /usr/share/selinux/devel/Makefile. In ScientificLinux yum provides /usr/share/selinux/devel/Makefile tells me that this already comes with the package selinux-policy You can now use this Makefile to compile the SELinux policy module. A good tip is to create an alias called semake like this:

alias semake='make -f /usr/share/selinux/devel/Makefile'

Still in your sshd_google_authenticator folder either use audit2allow to get the basic rules, or create them from scratch. A quick headsup about the file extensions;

  • .te is the type enforcement file. This contains all the rules and code to confine the application. This is the main file.
  • .fc is a list of paths and files that should get specific context. You can all of them using semange fcontext -l (That’s a small L).
  • .if interface file used for information for other domains communication. Don't mind this file for now.

Everything you really need is the .te file, and this is how it looks like in our sshd google authenticator module (it can be named anything, as long as it ends with .te).

# Name and version, every module should have this.
policy_module(sshd_google_authenticator, 0.0.1)

# List of the types, class and everything else you are going to use in your module that is not defined in this .te file.
# If you are getting any errors when you compile your module that it is unable to find a type, you probably forgot to declare it here.
require {
    type sshd_t;

# This is where we define our type. A good practise is to append _t for all types.
# This is the type we are going to give our .google_authenticator file.
type sshd_google_authenticator_t;

# What role our type should have. This is almost always going to be object_r
role object_r types sshd_google_authenticator_t;

# What sshd_t (the context the ssh daemon runs as) should be able to do with our type (sshd_google_authenticator_t),
# as a file. rename, create and unlink are base definitions, rw_file_perms is a set of rules.
# The rw_file_perms group is defined in /usr/share/selinux/devel/include/support/obj_perm_sets.spt with a lot of other
# groups. Reading this files give you a good overview of what they allow.
allow sshd_t sshd_google_authenticator_t:file { rename create unlink rw_file_perms };

# Without this, SELinux will be way too strict as default, as it won't know what this type really is.
# Remember that SELinux doesn’t only deal with files, but sockets and other filetypes as well.
# Leaving this out will still allow sshd_t to do its stuff, but you, in your shell will see a weird file.
# The only thing you will see is the file name. Even permissions will be hidden from you. (a fun trick to pull on your friends.. :] )
# An overview of this is located at

Now, when this file is created, create another file with the same name, but with the .fc extension. This should contain one line:

HOME_DIR/\.google_authenticator		--	gen_context(system_u:object_r:sshd_google_authenticator_t,s0)

This file have 3 parts. Path, type and context.

The first part is the path, on home directories, the home directory is replaced with HOMEDIR, this is all taken care of by SELinux and it generates this out of the home folders in passwd (as default). Don't use stuff like /home/*/.google_authenticator here.. It won't work as expected. However, on other directories not in anyones home directory should be find to enter here. Other than the magic HOME_DIR alias, there is HOME_ROOT, ROLE…, and user_… (I think). But this are almost never used, and the documentation for this is hard to get..

The second part is the type. -- means that this is a file. -d means it is a directory (there is more options as well). Nothing special about this.

The last part is what context our file should belong to, and the MLS level (the s0 part. Don't worry about this).

Now that you have this two files, check that it compiles using our semake alias. Just type semake. If you see something like this, congrats.

Compiling targeted sshd_google_authenticator module
/usr/bin/checkmodule:  loading policy configuration from tmp/sshd_google_authenticator.tmp
/usr/bin/checkmodule:  policy configuration loaded
/usr/bin/checkmodule:  writing binary representation (version 10) to tmp/sshd_google_authenticator.mod
Creating targeted sshd_google_authenticator.pp policy package
rm tmp/sshd_google_authenticator.mod.fc tmp/sshd_google_authenticator.mod

If not, go trough your two files and try to find the error. If your output was similar, you should have a file with the same name as the others with a .pp extension. This is your compiled module!

To enable it, type semodule -i sshd_google_authenticator.pp, and it should load. semodule -r sshd_google_authenticator.pp will remove the module. Also, note that loading the module will not change the context of your file. But selinux will now use your fc file when using restorecon, or other relabelling command. Use restorecon /home/username/.google_authenticator to do that now. ls -lZ /home/username/.google_authenticator to verify that the file have a new context.

And in case you wonder, your module will survive a reboot :)

Our module got a problem

Now that we have a module that work as expected, I have one good and one bad news. The bad one; our module won't solve our sshd google authenticator problem. The good one; you now hopefully know how to create your own basic modules for whatever you want.

The problem is not SELinux related, it works as it should. However, (as you might already have spotted), we need to give sshd unlink and rename permissions to our file. We can verify that the file is replaced with a new one looking at the inode on the file. ls -li /home/username/.google_authenticator before and after logging in using the one time password. The pid changes, the file is regenerated and created. Which means it looses its SELinux context, and ruin the whole plan.

SELinux doesn’t really care about file paths. I don't know if there is anyway to have a file getting it's default context like we want here, without putting it in another folder firs and make a rule that says "every file created in this folder should get our_context_t". New files created in your home folder will get the user_home_t context, which is NOT something we want sshd to have full control over. That would break the whole idea…

Update 18. Nov 2012: Look at the section We can use plan A after all to solve this problem without going to plan B.

Plan B

We already know that /home/username/.ssh is a folder that acts the way we want. So our plan b is to put our .google_authenticator file in that instead.

In the google authenticator libpam README, it said that we can manually set the location of the secret. Looks like our solution is simpler than first thought.

Replace the entry in our pam.d file with:

auth 	   required secret=/home/${USER}/.ssh/.google_authenticator

That’s all, it should now work perfectly in harmony with SELinux, even without our newly created module! This solution will for obvious reasons not work with the root user, because of the home path. But you should disable root login anyway. Use sudo!

We can use plan A after all (update 18. Nov 2012)

Thanks to Matthew Ife (#MatthewIfe) for pointing out this tip in the comment section.

If we use filetrans_pattern (which is available from Fedora 15+) we can get around the relabelling problem fairly elegant. Add

filetrans_pattern(sshd_t, user_home_dir_t, sshd_google_authenticator_t, file, ".google_authenticator")

To the button of your .te file, and it will make sure that all files that all files sshd_t creates in the folder with the type user_home_dir_t named .google_authenticator is labeled sshd_google_authenticator_t. This is just what we want!

If you do this, remember to

  • Don't add the secret option to pam.d
  • Add type user_home_dir_t; to the require {} section of your .te file.

One extra paranoid tip

Arguably, having the user access to the .google_authenticator file is a security risk. Since an attacker can steal or change the secret if they are able to get to your logged in terminal. Sure, but then you have already lost. But if you are using this for only one user, and want this extra paranoid setup. Check out this blog post at axivo, specially the Dark side part..